template:动态规划
背包问题
01背包
https://www.acwing.com/problem/content/2/
dp的过程可以理解为一个全局状态更新的过程, -
1,状态函数,记f[i][j]为前i个物品放入容量为j的背包的最大价值。 -
2,观察边界情况写转态转移,当前背包容量为j,考虑?第i个物品能否放入?是否放入?
若j<w[i],无法放入,则f[i][j]=f[i-1][j]
若j>=w[i],可以放入,判断要不要放入。f[i][j]=max(f[i-1][j]+f[i-1][j-v[i]]+w[i])
1 |
|
时间复杂度O(mn),无法优化,空间复杂度可以优化,事实上维度i可以利用滚动数组省去。
也就是说我们每次从后往前优化f[j],确保使用的状态都是上一层的状态,防止串联情况发生。
1
2
3
4
5
6
7for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
完全背包问题
完全背包就是在一维背包的基础上取消了对物品数量的限制。这对应着状态转移方程的变化。我们应当用刚刚更新过的值取更新现在的值
-
1.当前背包容量j<w[i],不能放入,则f[i][j]=f[i-1][j] -
2.当前背包容量j>=w[i],能放入,但要比较代价
(1)若第i件物品不放入背包,则 f[i][j]=f[i-1][j]
(2)若第i件物品放入背包,则 f[i][j]=f[i][j-w[i]]+c[i]
在选择第i件物品的情况下(无论多少),背包容量为j-w[i]时包含了放入放入了第i件物品的情况(因为从前往后更新的)。容量为j时还可以再放入第i件物品,所以用f[i][j-w[i]]更新f[i][j]
1 |
|
多重背包-朴素算法
朴素的思想是把多重背包转化为01背包处理,第i种物品可以取0件,1件…s[i]件,为此取第i种物品的情况分解为取s[i]种01背包的物品,每件体积为kv[i];价值为kw[i];
1 |
|
多重背包-二进制优化
二进制优化基于这样一个事实,一个数n一定可以由
一系列2的k次方的数相加表示,比如我们要取出50个苹果,我们可以分成几个箱子:(1,2,4,8,16,19),则0-50之间的任意个数都可以使用且仅使用上面的数一次后相加得到。基于这一事实,在多重背包问题中我们可以把第i件物品拆分为若干件物品,转化为01背包问题。例如si=12,拆分系数为1,2,4,5,对应(v1,wi),(2vi,2wi),4(vi,4wi),(5vi,5wi).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36#include<iostream>
using namespace std;
const int N = 1010;
const int M = 2010;
int v[N],w[N],s[N],vv[12*N],ww[12*N],n,m,cnt=0;
int f[M];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
for(int i=1;i<=n;i++)
{
int k=1;
while(k<=s[i])
{
cnt++;
vv[cnt]=k*v[i];
ww[cnt]=k*w[i];
s[i]-=k;
k=k*2;
}
if(s[i])
{
cnt++;
vv[cnt]=s[i]*v[i];
ww[cnt]=s[i]*w[i];
}
}
for(int i=1;i<=cnt;i++)
{
for(int j=m;j>=vv[i];j--)
f[j]=max(f[j],f[j-vv[i]]+ww[i]);
}
printf("%d",f[m]);
}
分组背包
n组物品和一个容量为m的背包,同一组中的物品最多选一个。
更新思路和01背包其实是一样的,不过每次更新j的时候,在当前j下试探组中每一个物品即可
由于只能取一次,滚动数组优化的时候j和01背包类似,从大到小更新。
1 |
|
线性dp
数字三角形
充分体现出数组开大的好处,没赋值的地方都是0,边界条件基本不用细致考虑。
1 |
|
时间复杂度O(n(n − 1)/2)
贪心+二分优化LIS
这个算法把时间复杂度优化到了O(nlogm),原因在于最大长度的维护使用了二分。具体的操作是这样的,维护一个b[len].
注意b不是最长上升子序列,但b的长度始终代表出现过的最长上升子序列长度。
比如我们看上图b由259到249的更新过程,LIS实际上由259变为了24,不删除9是有这样两种考虑
-
在本题数据中4代替了5,9没必要删去,让他呆在这里即可,后续如果由2,4发展出了更长的LIS,一定是踩过9向前发展的(本题6踩过了9)
- 如果9后面跟的是10,怎么办?没关系,因为4只是代替了5,由2 4 9 10和2 5 9
10构成的序列长度是一样的
无论如何,b的长度可以代表出现过的最长LIS的长度。
复习一下二分模版 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// l==r时退出while循环
// 全都是闭区间
// else后 l总是+1,r总是-1
// mid加不加1只取决于r=mid还是l=mid,只是为了防止l=mid出现死循环,l=1 r=2 mid=1.5=1 则l=mid=1死循环
// 不需要可以判断答案在哪一个区间,看看怎么写check能够使得 r=mid或者l=mid即可
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:比如找第一个大于等于五的数的位置
// if(a[mid]>=5) r=mid
// else l = mid+1
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1; //严格边界
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
//比如找最后一个小于等于5的数的位置, check就是 if a[mid]<=5
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1; //严格边界
}
return l;
}
本题
1 |
|
最长公共子序列
下面的图包含了更新f和记录序列的方式,利用了前驱数组p[i][j],可以通过·回溯找到最长公共子序列
下面给出求最长长度的代码 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#include<iostream>
#include<string>
using namespace std;
const int N =1010;
int f[N][N],n,m;
//f[i][j]表示用a的前i个数,b的前j个数
int main()
{
scanf("%d%d",&n,&m);
string a,b;
cin>>a>>b;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(a[i-1]==b[j-1]) f[i][j]=f[i-1][j-1]+1;
else f[i][j]=max(f[i-1][j],f[i][j-1]);
}
}
printf("%d",f[n][m]);
}
最短编辑距离
1 |
|
区间dp:石子划分
如何划分石子使得代价最小?
关键在于遍历的顺序和划分的理解。按照区间长度枚举,是因为dp更新较长区间的时候,必然会用到较短区间的结果,也就是状态集合的扩张。为此
- 大循环是len的枚举。
-
在给定的的len下枚举所有长为len的区间,因此中间循环是枚举左端点,从而枚举所有长为len的区间。
- 转态转移是考虑内部所有划分得到的最小值,因此枚举划分点k即可。
1 |
|
计数类dp-整数分解
1 |
|
树形dp
1 |
|