背包问题
DP问题:两个方面去考虑:
一、状态表示,如两维度$f(i,j)$;集合($f$代表什么集合)、属性(最大值、最小值、数量)。
集合:1、所有选法;2、条件 ①只从前$i$个物品中选;②总体积不超过j。
二、状态计算;
一般是对集合的划分:把集合$f(i,j)$分解为子集,对于背包问题,划分为包含第i个物品的,与不包含第i个物品的。
划分一般保持①不漏掉任何元素;②一般不重复
DP优化:一般是状态方程等价的变化。
01背包
每件物品最多只用1次
问题: https://www.acwing.com/problem/content/2/
$f(i,j) = max(f(i-1, j), f(i-1, j - v_i) + w_i)$
优化:$i$层的时候只依赖于$i-1$层,$j$层的选择永远有$ \le j$;因为更新依赖于 i - 1层,所以从j=m -->v_i
做更新。
1 |
|
优化版本:
1 |
|
完全背包
每件物品有无限个
集合:所有只考虑前i个物品,且总体积不大于j的所有选法;
属性:max
状态计算:集合的划分:按照第i个物品选多少个(k个)进行划分;
第i物品取k个:取前i-1个物品但不含第i个物品,同时去掉k个物品i,再加回来k个物品i;
- $f(i, j) = f(i-1, j - k v_i) + k w_i$
优化:
$f(i,j)=max(f(i-1, j), f(i-1, j - v) + w, f(i-1, j - 2v) + 2w,…,f(i-1, j - kv) + kw)$
$f(i, j - v) = max(f(i-1, j - v), f(i-1, j- 2v) + w, )$
相差一个$w$,所以可以用第二个式子取代第一个式子,那么便可以约掉k,得到:
$f(i,j)=max(f(i-1, j), f(i, j - v) + w)$
对比01背包$f(i,j)=max(f(i-1, j), f(i-1, j - v) + w)$,就是$i$的转移层数。
1 |
|
优化版本: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
using namespace std;
const int N = 1010;
int f[N][N];
int v[N], w[N]; //物品的体积和价值。
int n, m; //物品种数和背包容积
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
f[i][j] = f[i-1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
优化版本2:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using namespace std;
const int N = 1010;
int f[N];
int v[N], w[N]; //物品的体积和价值。
int n, m; //物品种数和背包容积
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= m; j++) //都是第i层,因此后面的f[j]依赖前面的f[j],所以先计算前面的
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
多重背包
每个物品有有限个
集合:所有只从前i个物品中选,并且总体积不超过j的选法。
属性:max。
状态计算:集合的划分:按照第i个物品选多少个来划分。
s个物品最多拆分为$logs$个物品。
$2^0、2^1、2^2、2^3….、2^k,c, c < 2^{k+1}$,可以凑出来$0 1234… 2^{k+1}-1$。
也就是第i个物品选$s_i$个,把第i个物品拆成$log S_i$个,原始复杂度$NVS$ 优化为 $NVlogS$
$f(i,j) = max(f(i-1, j - v[i] k) + w[i] k);k=0,1,…,s[i]$
优化为01背包:把Si个物品拆分,对拆分的进行01背包计算;时间复杂度为$O(NVlogS)$;
1 |
|
优化版本: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
37
38
39
40
41
42
using namespace std;
//N的取值为1000*log2000,M为2010
const int N = 12000, M = 2010;
int f[M];
int v[N], w[N];
int n, m;
int main()
{
cin >> n >> m;
int cnt = 0;
int vol, val, wei;
for(int i = 1; i <= n; i++)
{
cin >> vol >> val >> wei;
for(int k = 1; k <= wei; k <<=1)
{
cnt ++;
v[cnt] = k * vol;
w[cnt] = k * val;
wei -= k;
}
if (wei > 0)
{
cnt ++;
v[cnt] = wei * vol;
w[cnt++] = wei * val;
}
}
n = cnt;
for(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]);
cout << f[m] << endl;
return 0;
}
分组背包
物品有N组,每组里最多选一个物品。
状态表示 $f(i,j)$ :集合:只从前$i$组物品中选,且总体积不大于$j$的所有选法;
属性:max
状态计算:即集合划分:枚举第$i$组物品选哪个或者不选;
$f(i-1, j - v[i, k]) + w[i, k]$
1 | // $f(i-1, j - v[i, k]) + w[i, k]$ |
线性DP
数字三角形
状态表示 $f(i,j)$:① 表示的集合:是所有从起点走到$(i,j)$的路径; ②属性:路径之和的MAX
状态计算:集合划分:一类左上方、一类来自右上方的路径;左上方:$f(i-1, j - 1) + a(i, j)$;右上方:$f(i-1, j) + a(i, j)$
计算量的计算:状态数量 × 计算每一个状态需要的时间
1 |
|
最长上升子序列
- 状态表示$f(i)$:①表示的集合:所有以第i个数结尾的数值上升子序列的集合;②属性:集合中每一个上升子序列的长度的MAX
- 状态计算:集合划分:首先第i个数是确定的,倒数第二个数是什么$a_j$,那该上升子序列的最大长度为f(j) + 1,因此$a_i = max(f(j) + 1),j =0 , 1, 2,…i-1$
1 |
|
首先数组a中存输入的数(原本的数),开辟一个数组f用来存结果,最终数组f的长度就是最终的答案;假如数组f现在存了数,当到了数组a的第i个位置时,首先判断a[i] > f[cnt] ? 若是大于则直接将这个数添加到数组f中,即f[++cnt] = a[i];这个操作时显然的。
时间复杂度分析:$O(nlogn)$
1 |
|
最长公共子序列
- 状态表示$f(i,j)$:①表示的集合:所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母出现的公共子序列;②属性:MAX
- 状态计算:集合划分:根据选或者不选:$a(i)、b(j)$。共有四种情况。分为$f(i-1, j-1)、f(i-1, j -1) + 1$,同时选一个不选另一个$f(i-1, j)、f(i, j - 1)$,这两个是与前面的集合有重叠的,但是这两个集合包含了需要的解。此时可以去掉$f(i-1, j-1)$,因为前两个集合同时包含了该集合。
1 |
|
最短编辑距离
状态表示:集合:所有将$a[1- i]$变成$b[1-j]$的操作方式;
状态计算:按照最后一个字符做什么操作进行划分:
①增:$f(i, j-1) + 1$ ;②删:$f(i-1,j) + 1$ ③:改:$f(i-1, j - 1) + 1$
1 |
|
编辑距离
给n个字符串,再给出查询字符串与长度,问不超过长度的情况下有多少个字符串可以与该查询字符串匹配。
1 |
|
区间DP
一般区间长度从小到大来循环。
石子合并
- 状态表示$f(i,j)$:①表示的集合:所有将第 $i$ 堆石子到第 $j$ 堆石子合成一堆石子的合并方式;②属性:Min
- 状态计算:集合划分:最后一次分界线的位置来分类:$f(i, j)= min(f(i, k) + f(k + 1, j) + s[j] - s[i - 1]),k=i到j-1$
1 | //最后对区间长度进行枚举 |
计数类DP
前面的都是求最值,这个求的是个数。
整数划分
解法1:完全背包的解法;
集合:所有总和是$i$,并且恰好表示成$j$个数的和的方案;那么有f(0)=1
,即一个都不选,总和是0的方案数为1.
而f(1)
表示一个都不选,方案数为1,则不存在为0.
1 |
|
解法2:集合:所有总和是$i$,并且恰好表示成$j$个数的和的方案;属性:数量;
状态计算:$f(i, j)$:可以表示的方案中的最小值是1,最小值大于1。
左边是$f(i-1, j - 1)$;右边的每一个方案都减去1,那便等价于 $f(i-j, j)$ ;
最后对全部的$f$ 求和。
1 |
|
数位统计DP
计数问题
分情况讨论:对于区间内的次数,考虑前缀和的思想。
实现count(n, x)
:实现1到n之间总共出现x的个数。比如要求出1在每一位上出现的次数。
设n =abcdefg
,求1在第四位上出现的次数,即转换为:1<=xxx1yyy <=abcdefg
状态压缩DP
蒙德里安的梦想
等价于按照列来横着放置小方块。
$f(i,j)$:摆放第i列,i-1列伸出来横着的方格状态为j的方案数,j为一个二进制数;
i-1列转移到i列满足:(j & k) == 0,其中k是i-1列的状态;
同时每个有效的状态满足:j | k 不存在连续奇数个零,即每个格子只能用纵向的格子来填;
- 位运算基础:
& | \ | ^ | << | >> | |
---|---|---|---|---|---|
并 | 或 | 异或 | 右移 | 左移 | |
1&1 = 1 | 1\ | 0 =1 | 1^0 = 1 | 1<<1 = 10 | 10 >> 1 = 1 |
1&0 = 0 | 0\ | 0 = 0 | 1^1 = 0 | 11 << 1 = 110 | 101 >> 1 = 10 |
1 |
|
Hamilton路径
- 1、状态表示 :
dp(i, j)
集合:已经走过的所有点的集合为i
,移动到j
。 - 比如i=1110011,表示点1、2、5、6、7走过了,点3、4没走过。
- 状态计算:划分:倒数第二个点对把
f(i,j)
分类。倒数第二个点是k
,那路径0-->k->j
,那么走过点的集合i
中去除掉点j
,然后加上从k到j的权值;最后的答案应该是dp[111..11][n-1]
表示0到n-1都走过了,下一个点走到n-1的最短路径。 dp[i][j] = min{dp[i][j], dp[i - (1 << j)][k] + map[k][j]}
1 |
|
树形DP
没有上司的舞会
状态表示:集合
f(u,0)
:所有从以u
为根的子树中选择,并且不选u
这个点的方案;f(u,1)
:所有从以u
为根的子树中选, 并且选择u
的方案。属性:MAX
状态计算:比如:
f(u, 0) = sum_i (max(f(s_i, 0) , f(s_i, 1)))
,s为所有子树的求和;f(u, 1) = sum_i(f(s_i, 0))
。复杂度为枚举子树节点的数目,即边的数量:$O(n)$。
1
2
3
4
5---u---
| |
| |
| |
s1 s2
1 |
|
记忆化搜索
滑雪
- 对四个方向进行dp,每个dp返回当前位置的最大值,当到达计算过的位置便直接返回里面的值,是记忆化存储的一种实现方式。
1 |
|