状态本质是图论中一个个点,转移对应一条条边
复杂度分析:状态数量 × \times × 状态计算
线性DP 1.状态题目中有很多状态,而这些状态间存在某些关系的题目。
所谓状态,就是你当前处于哪个场景的概念。
比如我现在手里有六个骰子,
那么就有 6 × 6 × 6 {6}\times{6}\times{6} 6 × 6 × 6 也就是 216 216 2 1 6 种场景,
变量的组合便是状态。
2.转移方程状态与状态之间的关系(点与点之间的边)。
要根据具体题目进行设计。
典例这是最常见的一个递推。
那么怎么进行DP
呢?
想想状态和什么有关系——当前这一项和前两项有关系,后两项与当前状态有关系。
那么就有了DP
的两种写法:
1.自己求别人
2.别人求自己
根据题目选择不同的 DP \text{DP} DP 方式
3.记忆化搜索
复杂度 O ( N ) O(N) O ( N )
特征方程法解通项公式。
闫式思考法 矩阵路线问题状态表示f i , j f_{i,j} f i , j 表示一类集合 :所有从 ( 1 , 1 ) (1,1) ( 1 , 1 ) 到 ( i , j ) (i,j) ( i , j ) 的路线。 属性 :最大值/最小值/方案数。 状态计算集合的划分划分依据:最后 ,本例中为最后一步从上面下来还是从左边过来。从上边过来:( 1 , 1 ) → ( i − 1 , j ) → ( i , j ) (1,1)\to(i-1,j)\to(i,j) ( 1 , 1 ) → ( i − 1 , j ) → ( i , j ) 从左面过来:( 1 , 1 ) → ( i , j − 1 ) → ( i , j ) (1,1)\to(i,j-1)\to(i,j) ( 1 , 1 ) → ( i , j − 1 ) → ( i , j ) 两部分取一个 max \max max ,分而治之,将集合进行一个划分。 划分原则: 计算顺序按照拓扑序计算,保证每个状态计算时它的依赖状态已经被计算过了。 截图:
方格取数
LIS状态表示集合:所有以 a i a_i a i 结尾的严格单调上升子序列 属性:Max/Min/数量 状态计算集合——分而治之。 通过最后一步 划分:最后一步是 a i a_i a i A1017
正序倒叙分别求两遍最长上升子序列取最大值即可。
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 #include <bits/stdc++.h> using namespace std;int n, a[110 ], f[110 ];int main () { int t; scanf ("%d" , &t); while (t--) { scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) scanf ("%d" , &a[i]); int res = 0 ; for (int i = 1 ; i <= n; i++) { f[i] = 1 ; for (int j = 1 ; j < i; j++) if (a[i] > a[j]) f[i] = max (f[i], f[j] + 1 ); res = max (res, f[i]); } for (int i = n; i >= 1 ; i--) { f[i] = 1 ; for (int j = n; j > i; j--) if (a[i] > a[j]) f[i] = max (f[i], f[j] + 1 ); res = max (res, f[i]); } printf ("%d\n" , res); } return 0 ; }
A1014
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 #include <bits/stdc++.h> using namespace std;int f[1100 ];int g[1100 ];int n;int a[1100 ];int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) scanf ("%d" , &a[i]); for (int i = 1 ; i <= n; i++) { f[i] = 1 ; for (int j = 1 ; j < i; j++) if (a[i] > a[j]) f[i] = max (f[i], f[j] + 1 ); } for (int i = n; i >= 1 ; i--) { g[i] = 1 ; for (int j = n; j > i; j--) if (a[i] > a[j]) g[i] = max (g[i], g[j] + 1 ); } int res = 0 ; for (int i = 1 ; i <= n; i++) res = max (res, f[i] + g[i] - 1 ); printf ("%d\n" , res); return 0 ; }
A1012 友好城市
两岸,一边看作自变量,一边看作因变量,在自变量不断递增的过程中,因变量也要是单调递增的,对自变量排下序,对因变量求最长上升子序列,这样就转化成了一个最长上升子序列问题。
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 #include <bits/stdc++.h> using namespace std;const int N = 5005 ;typedef pair<int , int > PII;PII q[N]; int f[N];int main () { int n; scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) scanf ("%d%d" , &q[i].first, &q[i].second); sort (q + 1 , q + n + 1 ); int res = 0 ; for (int i = 1 ; i <= n; i++) { f[i] = 1 ; for (int j = 1 ; j < i; j++) if (q[i].second > q[j].second) f[i] = max (f[i], f[j] + 1 ); res = max (res, f[i]); } printf ("%d\n" , res); return 0 ; }
A1016 最大上升子序列和
状态表示f i f_i f i 集合:所有以 a i a_i a i 结尾的上升子序列 属性:和的最大值 状态计算:倒数第二个数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <bits/stdc++.h> using namespace std;const int N = 1010 ;int n;int a[N], f[N];int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) scanf ("%d" , &a[i]); int ans = 0 ; for (int i = 1 ; i <= n; i++) { f[i] = a[i]; for (int j = 1 ; j < i ; j++) if (a[i] > a[j]) f[i] = max (f[i], f[j] + a[i]); ans = max (ans, f[i]); } cout << ans << endl; return 0 ; }
导弹拦截贪心流程:情况1:如果现有的子序列结尾都小于当前数,那么就创建新的子序列 情况2:把当前数放到结尾大于等于它的最小的子序列后面。因为要把结尾尽可能大的保持住,这样后面才会有更多的机会。 证明贪心正确性如何证明两个数相等?A表示贪心算法得到的序列个数,B表示最优解 A ≤ B , B ≤ A A\le B,B\le A A ≤ B , B ≤ A 由于 B 是最优解,因此 B ≤ A B\le A B ≤ A 使用调整法,假设最优解对应的方案和当前方案不同,那么必然会存在第一个不同的位置。贪心法一定会把这个数分配到一个大于等于它的最小的子序列的后面,最优解也会放到某个序列后面,它们之后的序列就可以是一样的,就可以把它们交换,使得贪心法成为最优解,同时没有增加子序列的个数,所以 A ≤ B A\le B A ≤ B 。 此题得证。 A1010
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 #include <bits/stdc++.h> using namespace std;const int N = 1010 ;int n;int q[N];int f[N], g[N];int main () { while (cin >> q[n]) n++; int res = 0 ; for (int i = 0 ; i < n; i++) { f[i] = 1 ; for (int j = 0 ; j < i; j++) if (q[j] >= q[i]) f[i] = max (f[i], f[j] + 1 ); res = max (res, f[i]); } cout << res << endl; int cnt = 0 ; for (int i = 0 ; i < n; i++) { int k = 0 ; while (k < cnt && g[k] < q[i]) k++; g[k] = q[i]; if (k >= cnt) cnt++; } cout << cnt << endl; return 0 ; }
A187
每个位置有两种选择,两种决策,要考虑所有情况,没办法归类,只能直接爆搜。
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 43 44 45 46 47 #include <bits/stdc++.h> using namespace std;const int N = 55 ;int n;int q[N];int up[N], down[N]; int ans;void dfs (int u, int su, int sd) { if (su + sd >= ans) return ; if (u == n) { ans = su + sd; return ; } int k = 0 ; while (k < su && up[k] >= q[u]) k++; int t = up[k]; up[k] = q[u]; if (k < su) dfs (u + 1 , su, sd); else dfs (u + 1 , su + 1 , sd); up[k] = t; k = 0 ; while (k < sd && down[k] <= q[u]) k++; t = down[k]; down[k] = q[u]; if (k < sd) dfs (u + 1 , su, sd); else dfs (u + 1 , su, sd + 1 ); down[k] = t; } int main () { while (cin >> n, n) { for (int i = 0 ; i < n; i++) cin >> q[i]; ans = n; dfs (0 , 0 , 0 ); cout << ans << endl; } return 0 ; }
最长公共子序列状态表示f i , j f_{i,j} f i , j 集合:所有由第一个序列的前 i i i 个字母,第二个序列的前 j j j 个字母且以 b j b_j b j 结尾的公共上升子序列 属性:最大长度 状态计算分而治之,对每一个部分求最大值。 所有包含 a i a_i a i 的公共上升子序列根据倒数第二个元素划分。 f i , k f_{i,k} f i , k 所有不包含 a i a_i a i 的公共上升子序列:f i − 1 , j f_{i-1,j} f i − 1 , j 优化DP 的优化一般思路不变,只是对代码进行等价变形。
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 #include <bits/stdc++.h> using namespace std;const int N = 3010 ;int n, res;int a[N], b[N];int f[N][N];int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) scanf ("%d" , &a[i]); for (int i = 1 ; i <= n; i++) scanf ("%d" , &b[i]); for (int i = 1 ; i <= n; i++) { int maxv = 1 ; for (int j = 1 ; j <= n; j++) { f[i][j] = f[i - 1 ][j]; if (a[i] == b[j]) f[i][j] = max (f[i][j], maxv); if (b[j] < a[i]) maxv = max (maxv, f[i - 1 ][j] + 1 ); } } for (int i = 1 ; i <= n; i++) res = max (res, f[n][i]); cout << res << endl; return 0 ; }
组合数问题(杨辉三角)
过河卒但是没有🐎
每个点的方案数等于左边数和上边数相加(记得初始化边界)。
那么这个东西斜起来看的话其实就是杨辉三角。
数字三角形 正常版洛谷上有道滑雪 ,
第i i i 行有i i i 个数,
每次可以朝下或者朝右下走,
那么从第一行走到最后一行怎么走会使路径上的数和最大,
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − 1 ] , f [ i − 1 ] [ j ] ) f[i][j]=max(f[i-1][j-1],f[i-1][j]) f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − 1 ] , f [ i − 1 ] [ j ] ) 。
还是要记得初始化 。
EX版附加条件:% 100 \%100 % 1 0 0 后最大
可以加一个条件,使得每个状态都% 100 \%100 % 1 0 0 ?
NO
会破坏它的最优子结构
判断最优子结构 就是判断 DP 正确性的关键
也就是最优解不一定是用你所谓的最优解求出的
重点来了 :DP
题中题目每多一个条件,状态都可以再加一个维度
f [ i ] [ j ] [ k ] f[i][j][k] f [ i ] [ j ] [ k ] :走到i i i ,j j j ,和% 100 \%100 % 1 0 0 为k k k 这件事是否可能(true/false)
数字三角形3必须经过 (n 2 \dfrac{n}{2} 2 n , n 2 \dfrac{n}{2} 2 n ) 这个点。
数字三角形4必须先走到某个点再走到最底层
个人想法:
把这看作两个过程,相当于先以这个点为终点跑一遍数字三角形,然后再以这个点为起点再跑一遍数字三角形,两段相加即可。
最长上升子序列(Longest Increasing Subsequence) 1.求最长长度f [ i ] f[i] f [ i ] 代表 i i i 必选且 i i i 为最后一个数得到的最长上升子序列。
f [ i ] f[i] f [ i ] 至少等于 1 1 1 ,因为至少选 i i i 。
那么就可以求最长:
2.求方案数再开一个 g [ i ] g[i] g [ i ] ,
g [ i ] g[i] g [ i ] 代表以 i i i 结尾最长的长度的方案数,
这是通解。
如下:
遇到求方案数的问题都可以新开一个数组来记录方案数。
假如长度变长了,那么就 g i = g j g_i=g_j g i = g j 。
改一下写法的话就是像上图所示,如果是更新那就先置为0并且令它们相同,然后顺其自然的加上,相当于赋值了,而等于的情况正好也是加上,也就是稍微合并了一下操作。
3.输出一种方案数再开一个数组p r e pre p r e 记录每一个状态是从哪里走过来的。 p r e [ i ] pre[i] p r e [ i ] =0就是前面没东西了
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 #include <bits/stdc++.h> using namespace std;int f[10000 ];int g[10000 ];int pre[10000 ];int main () { for (int i=1 ;i<=n;i++) { f[i]=1 ; g[i]=1 ; pre[i]=0 ; for (int j=1 ;j<i;j++) if (a[j]<a[i]) { int l=f[j]+1 ; if (l>f[i]) { f[i]=l; g[i]=0 ; pre[i]=j; } if (l==f[i]) { g[i]+=g[j]; } } } int p,cnt=0 ; do { z[++cnt]=p; p=pre[p]; }while (p!=0 ); reverse (z+1 ,z+cnt+1 ); return 0 ; }
复杂度显然是n 2 n^2 n 2 的。
4.P2501题解
5.优化要做到n l o g n nlogn n l o g n ?
1.线段树我们无非就是要在 1 ∼ i − 1 1\sim{i-1} 1 ∼ i − 1 中找到一个 j j j 使得 a j < a i a_j<a_i a j < a i 并且 f [ j ] f[j] f [ j ] 最大
那我们就建一颗线段树,范围 1 ∼ m 1{\sim}m 1 ∼ m
m m m 是 m a x ( a 1 ∼ a n ) max({a_1}\sim{a_n}) m a x ( a 1 ∼ a n ) ,即所有数的最大值
假如 a [ j ] , f [ j ] a[j],f[j] a [ j ] , f [ j ] 都知道了
进行一个单点修改
每次找到一个 f [ j ] f[j] f [ j ] 就把它赋值给 a j a_j a j ,
这样当前询问 a i a_i a i 只需要询问 1 ∼ a i − 1 1\sim a_i-1 1 ∼ a i − 1 区间中的最大值,
因为这样保证了之前的数都是小于 a i a_i a i 的并且它们的 LIS 都已经求出。
询问的是什么?
从左向右扫的
询问的是所有小于 a i a_i a i 的数
这样找到最大的 f j f_j f j
满足 a j < a j a_j<a_j a j < a j
还取到 a j a_j a j 的最大值
那么 a [ i ] a[i] a [ i ] 就是 f [ j ] + 1 f[j]+1 f [ j ] + 1
2.二分 插播一条二分关于c++的lower_bound与upper_bound函数的理解 )
当容器中的元素按照递增的顺序存储时,lower_bound函数返回容器中第一个大于等于 目标值的位置,upper_bound函数返回容器中第一个大于 目标值的位置。若容器中的元素都比目标值小则返回最后一个元素的下一个位置。(≥ \ge ≥ )
如果容器中的元素是递减的应该怎么查找呢?这是可以借助c++内置的仿函数greater<data_type>(),相当于重新定义了比较规则。此时lower_bound_()查找的是容器中第一个小于等于 目标值的元素的位置,而upper_bound()查找的是容器中第一个小于 目标值的元素的位置就。如果容器中的元素都比目标值大则返回最后一个元素的下一个位置。(> > > )
详情见优先队列
解法现在存在两个位置p 1 p1 p 1 和p 2 p2 p 2 且p 1 < p 2 p_1<p_2 p 1 < p 2 、a p 1 a_{p_1} a p 1 >a p 2 a_{p_2} a p 2
f [ p 1 ] f[p_1] f [ p 1 ] ≤ \le ≤ f [ p 2 ] f[p_2] f [ p 2 ]
这意味着什么
这个p 1 p_1 p 1 肯定是没有用的
p 2 p_2 p 2 一定比p 1 p_1 p 1 更优
这样就可以把p 1 p_1 p 1 删掉了
栗子: 7 2 1 5 6 4 3 8 9
假如
f [ 7 ] = f [ 2 ] = 1 f[7]=f[2]=1 f [ 7 ] = f [ 2 ] = 1
那么7
就没有用了
没用的就给去了
在这里4
可以把5
给替换掉
在这里4
又被替换成3
我们发现什么问题?
这个创造出来的序列的第i i i 个位置就是f [ i ] f[i] f [ i ]
比如a 3 a_3 a 3 就是最长上升子序列为3 3 3 的最小的那个(最优的)
cnt
代表新建出来的序列的大小
a a a 代表建出来的序列
a [ 3 ] = f [ a [ 3 ] ] = 3 a[3]=f[a[3]]=3 a [ 3 ] = f [ a [ 3 ] ] = 3
但是这样做还是n 2 n^2 n 2 的
z [ i ] z[i] z [ i ] 存的是f [ i ] f[i] f [ i ] 的下标
也就是z [ f [ i ] ] = i z[f[i]]=i z [ f [ i ] ] = i
算出了f [ i ] f[i] f [ i ] 那肯定就把f [ i ] f[i] f [ i ] 放在新数组的第f [ i ] f[i] f [ i ] 个位置
比如算出了f [ 8 ] = 4 f[8]=4 f [ 8 ] = 4 ,那么新数组的第4 4 4 个位置就是4 4 4
新数组就是z [ ] z[] z [ ] ,它存的不是新的序列,而是新序列中每个数对应原序列的下标
答案就是z 1 − c n t z_{1-cnt} z 1 − c n t
f [ z [ j ] ] = j f[z[j]]=j f [ z [ j ] ] = j
代码:
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 #include <bits/stdc++.h> using namespace std;int z[233 ];int f[233 ];int a[233 ];int main () { int cnt=0 ; for (int i=1 ;i<=n;i++) { f[i]=1 ; for (int j=1 ;j<=cnt;j++) { if (a[z[j]]<a[i]) f[i]=max (f[i],j+1 ); } if (f[i]>cnt) { cnt++; z[cnt]=i; } else { if (a[i]<a[z[f[i]]]) z[f[i]]=i; } } return 0 ; }
1 else if (a[i]<a[z[f[i]]]) z[f[i]]=i;
这句就是把5
给更新成4
注意:这个新构造的序列并不是最长上升子序列,而是用来更新f[i]的
如果要方案的话就开个数组记录一下
但是这样解的个数不是很好算
再优化可以用二分查找找出小于a[i]
的最大的那个位置
加入一个二分替代枚举就可以达到n l o g n nlog_n n l o g n
因为新构造的序列是按照f[]
值单调递增的
所以我们只需要每次二分查找一个小于当前a[i]
的最大的数就可以了
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <bits/stdc++.h> using namespace std;const int N=1e5 +50 ;int z[N];int f[N];int a[N];int ef (int l,int r,int k) { while (l<r) { int mid=(l+r+1 )>>1 ; if (a[z[mid]]<a[k]) l=mid; else r=mid-1 ; } return l; } int main () { int n; scanf ("%d" ,&n); for (int i=1 ;i<=n;i++) { scanf ("%d" ,&a[i]); } int cnt=0 ; for (int i=1 ;i<=n;i++) { f[i]=1 ; f[i]=max (f[i],ef (0 ,cnt,i)+1 ); if (f[i]>cnt) { cnt++; z[cnt]=i; } else { if (a[i]<a[z[f[i]]]) z[f[i]]=i; } } printf ("%d " ,n-cnt); return 0 ; }
二分真的很难调
简单解释一下吧,
现在保证左端点满足条件,
不断微调右端点,
二分到最后让右端点与左端点合并,
而最后遇到相邻的情况,
mid要取右中位数(也就是如果是两个相邻的数,在取整的过程中取右端点)。
具体是不是这样我也说不清。
以上做法可以A掉P3902
练习题 滑雪:一个 n n n 行 m m m 列的网格图,每个格子有一个高度,只能滑向四周比自己矮的格子,任选起点,问最多能滑多远。
从高往低滑和从低向高滑的答案是一样的。
用 f i , j f_{i,j} f i , j 表示从 i , j i,j i , j 出发的最长长度。
即 f i , j = m a x ( f x , y ) + 1 f_{i,j}=max(f_{x,y})+1 f i , j = m a x ( f x , y ) + 1 ,其中 x , y x,y x , y 为能滑到 i , j i,j i , j 的格子。
就这样记忆化搜索。
可以将所有点的高度从小到大排序。
这一定是从排序后的数组的左边滑到右边。
那就可以从左向右进行 dp。
f i f_i f i 代表滑到 x i , y i x_i,y_i x i , y i 这个位置时的最长长度,然后枚举 i i i 左边的数并且满足相邻(∣ x i − x j ∣ + ∣ y i − y j ∣ = 1 |x_i-x_j|+|y_i-y_j|=1 ∣ x i − x j ∣ + ∣ y i − y j ∣ = 1 )的数中,f f f 值最大的那个,把它加一就可以更新 f i f_i f i 。
这样就可以保证转移是从左向右的,枚举 i , j i,j i , j 就可以了。
最低点初始化为 0。
这个方法本质上就是拓扑排序+DP。
乌龟棋在走的过程中,有六个变量在发生变化,其中五个可以用来表示状态,一个用来表示状态的值,即 f i , a 1 , a 2 , a 3 , a 4 f_{i,a_1,a_2,a_3,a_4} f i , a 1 , a 2 , a 3 , a 4 ,其中第一维表示当前的位置,后面四维表示分别用了多少牌。
然后我们发现第一维其实没有必要,因为当前所在的位置可以通过后四维求解,于是就可以减少枚举,此谓之去除冗余状态 。
至此的一些技巧:
状态设计(每有一个变量对应一个维度) 增加维度 求方案数 改变枚举顺序 消除冗余状态 背包DP 01背包状态表示f i , j f_{i,j} f i , j 集合:所有只从前 i i i 个物品中选,且总体积不超过 j j j 的选法的集合。 属性:最大值/最小值 划分依据:用最后一步来划分。 状态计算:不选第 i i i 个物品的所有方案 选第 i i i 个物品的所有方案 有n n n 件物品,一个背包,每个物品有重量和价值,在背包不超重的情况下,问得到价值的最大值。
设计DP状态 我们令d p i , j dp_{i,j} d p i , j 代在前i i i 个物品中进行选择,用了j j j 的体积所得到的最大价值之和。
转移方程 第i i i 个物体重量为w i w_i w i ,价值为v i v_i v i 。
考虑完了前i − 1 i-1 i − 1 件物品后,我们开始考虑第i i i 件物品,这件物品可以选也可以不选,那么就可以在这两种情况中取较大值。
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])
滚动优化 我们发现更新第i i i 个物品的时候只会用到第i − 1 i-1 i − 1 行,之前的都相当于没有用了,所以就可以把第一维去掉,用旧状态代表i − 1 i-1 i − 1 行,用新状态代表第i i i 行,更新新状态的时候就是拿旧时的自己来更新现在的自己,根据转移方程,我们发现其实是拿体积较小的状态递推为体积较大的状态,所以我们更新的时候需要保留旧状态,所以从后向前遍历,这样就可以保证用来更新新状态的状态全部都是旧状态;若正向遍历,体积小的旧状态已经被更新为新状态,后面更新体积大的新状态会用被覆盖的新状态来更新,导致考虑了第i i i 个物品后接着又考虑了第i i i 个物品,这样就会导致每个物品会被拿多次(无穷背包)。
1 2 3 for (int i=1 ;i<=n;i++) for (int j=m;j>=w[i];j--) f[j]=max (f[j],f[j-w[i]]+v[i]);
第二层循环的终止条件为j>=a[i]
,因为之后无法转移,也就是无法装下第i i i 件物品。
习题:
P1060
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <bits/stdc++.h> using namespace std;const int N=1e5 ;int n,m;int x[N],f[N],v[N],p[N];int main () { scanf ("%d%d" ,&n,&m); for (int i=1 ;i<=m;i++) { scanf ("%d%d" ,&v[i],&p[i]); x[i]=v[i]*p[i]; } for (int i=1 ;i<=m;i++) for (int j=n;j>=v[i];j--) { f[j]=max (f[j],f[j-v[i]]+x[i]); } printf ("%d" ,f[n]); }
P1048
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 43 44 45 #include <bits/stdc++.h> using namespace std;int f[2005 ];int w[100000 ];int v[100000 ];int main () { int n,m; cin>>n; cin>>m; for (int i=1 ;i<=m;i++) cin>>v[i]>>w[i]; for (int i=1 ;i<=m;i++) for (int j=n;j>=v[i];j--) f[j]=max (f[j],f[j-v[i]]+w[i]); cout<<f[n]; return 0 ; }
求方案数再开一个 g i , j g_{i,j} g i , j 代表当前状态的方案数
A11
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 #include <bits/stdc++.h> using namespace std;const int N = 1010 ;const int MOD = 1e9 + 7 ;int n, m;int f[N], g[N];int main () { cin >> n >> m; memset (f, 0xc0 , sizeof (f)); f[0 ] = 0 ; g[0 ] = 1 ; for (int i = 0 ; i < n; i++) { int v, w; cin >> v >> w; for (int j = m; j >= v; j--) { int maxv = max (f[j], f[j - v] + w); int cnt = 0 ; if (maxv == f[j]) cnt += g[j]; if (maxv == f[j - v] + w) cnt += g[j - v]; g[j] = cnt % MOD; f[j] = maxv; } } int res = 0 ; for (int i = 0 ; i <= m; i++) res = max (res, f[i]); int cnt = 0 ; for (int i = 0 ; i <= m; i++) if (res == f[i]) cnt = (cnt + g[i]) % MOD; cout << cnt << endl; return 0 ; }
求方案开一个 p r e i , j pre_{i,j} p r e i , j 表示 i , j i,j i , j 这个状态是由 i − 1 i-1 i − 1 的哪个状态转移而来就好。
A12
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 #include <bits/stdc++.h> using namespace std;const int N = 1005 ;int n, m;int f[N][N];int v[N], w[N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i] >> w[i]; for (int i = n; i >= 1 ; i--) for (int j = 0 ; j <= m; j++) { f[i][j] = f[i + 1 ][j]; if (j >= v[i]) f[i][j] = max (f[i][j], f[i + 1 ][j - v[i]] + w[i]); } int j = m; for (int i = 1 ; i <= n; i++) if (j >= v[i] && f[i][j] == f[i + 1 ][j - v[i]] + w[i]) { cout << i << ' ' ; j -= v[i]; } return 0 ; }
A1013
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 #include <bits/stdc++.h> using namespace std;const int N = 11 , M = 17 ;int n, m;int f[N][M];int w[N][M];int way[N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) cin >> w[i][j]; for (int i = 1 ; i <= n; i++) for (int j = 0 ; j <= m; j++) { f[i][j] = f[i - 1 ][j]; for (int k = 1 ; k <= j; k++) f[i][j] = max (f[i][j], f[i - 1 ][j - k] + w[i][k]); } int k = m; for (int i = n; i >= 1 ; i--) for (int j = 0 ; j <= k; j++) if (f[i][k] == f[i - 1 ][k - j] + w[i][j]) { way[i] = j; k -= j; break ; } cout << f[n][m] << endl; for (int i = 1 ; i <= n; i++) cout << i << ' ' << way[i] << endl; return 0 ; }
二维费用的背包问题状态表示f i , j , k f_{i,j,k} f i , j , k 集合:所有只从前 i i i 个物品中选,并且总体积不超过 j j j ,总重量不超过 k k k 的所有选法。 属性:最大值 状态计算最后一步:最后一个物品,要么包含要么不包含。 不包含:f i − 1 , j , k f_{i-1,j,k} f i − 1 , j , k 包含:f i − 1 , j − v i , k − m i + w i f_{i-1,j-v_i,k-m_i}+w_i f i − 1 , j − v i , k − m i + w i 初始化分类初始化分类体积最多是 j j j 方案数:f 0 , i = 1 , i ∈ [ 0 , m ] ∣ f 1 = 1 f_{0,i}=1,i\in{[0,m]}\quad|\quad f_{1}=1 f 0 , i = 1 , i ∈ [ 0 , m ] ∣ f 1 = 1 最大价值:f i , k = 0 , i ∈ [ 0 , n ] , k ∈ [ 0 , m ] ∣ f i = 0 , i ∈ [ 0 , m ] f_{i,k}=0,i\in[0,n],k\in[0,m]\quad|\quad f_i=0,i\in[0,m] f i , k = 0 , i ∈ [ 0 , n ] , k ∈ [ 0 , m ] ∣ f i = 0 , i ∈ [ 0 , m ] 体积恰好是 j j j 方案数:f 0 , 0 = 1 ∣ f 0 = 1 f_{0,0}=1\quad|\quad f_0=1 f 0 , 0 = 1 ∣ f 0 = 1 最大价值:f 0 , 0 = 0 , f 0 , i = − ∞ , i ∈ [ 1 , ∞ ] ∣ f 0 = 0 , f i = − ∞ f_{0,0}=0,f_{0,i}=-\infty,i\in[1,\infty]\quad|\quad f_0=0,f_i=-\infty f 0 , 0 = 0 , f 0 , i = − ∞ , i ∈ [ 1 , ∞ ] ∣ f 0 = 0 , f i = − ∞ 最小价值:f 0 , 0 = 0 , f 0 , i = ∞ , i ∈ [ 1 , ∞ ] ∣ f 0 = 0 , f i = ∞ f_{0,0}=0,f_{0,i}=\infty,i\in[1,\infty]\quad|\quad f_0=0,f_i=\infty f 0 , 0 = 0 , f 0 , i = ∞ , i ∈ [ 1 , ∞ ] ∣ f 0 = 0 , f i = ∞ 体积至少是 j j j 方案数:f 0 , 0 = 1 ∣ f 0 = 1 f_{0,0}=1\quad|\quad f_0=1 f 0 , 0 = 1 ∣ f 0 = 1 最小价值:f 0 , 0 = 0 , f 0 , i = ∞ , i ∈ [ 1 , ∞ ] ∣ f 0 = 0 , f i = ∞ f_{0,0}=0,f_{0,i}=\infty,i\in[1,\infty]\quad|\quad f_0=0,f_i=\infty f 0 , 0 = 0 , f 0 , i = ∞ , i ∈ [ 1 , ∞ ] ∣ f 0 = 0 , f i = ∞ 从集合的角度考虑,考虑前 0 个物品且花费体积为 0 这一个元素,或者说是状态,在不同集合的定义中可以被划分到哪个集合,剩下的集合的状态是如何。
AcWing1020 潜水员
状态表示集合:所有从前 i i i 个物品中选,且氧气至少是 j j j ,但其至少是 k k k 的所有选法。 属性:最小值 状态计算划分依据:最后一个物品的选择情况。 所有不含第 i i i 个物品的所有选法:f i − 1 , j , k f_{i-1,j,k} f i − 1 , j , k 所有包含第 i i i 个物品的所有选法:f i − 1 , j − v 1 , k − v 2 + w + i f_{i-1,j-v_1,k-v_2}+w+i f i − 1 , j − v 1 , k − v 2 + w + i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <bits/stdc++.h> using namespace std;const int N = 22 , M = 80 ;int n, m, k;int f[N][M];int main () { cin >> n >> m >> k; memset (f, 0x3f , sizeof (f)); f[0 ][0 ] = 0 ; while (k--) { int v1, v2, w; cin >> v1 >> v2 >> w; for (int j = n; j >= 0 ; j--) for (int k = m; k >= 0 ; k--) f[j][k] = min (f[j][k], f[max (0 , j - v1)][max (0 , k - v2)] + w); } cout << f[n][m] << endl; return 0 ; }
无穷背包按照直观的想法,那就再枚举一个 k k k 表示这个物品选多少个。
但这样复杂度是 O ( n 3 ) O(n^3) O ( n 3 ) 的,所以考虑优化。
对比01背包,最大的不同就是它可以通过自己来更新自己,所以我们只需要在原来的基础上加上这样一句话:
它可以自己更新自己。
自己最初没选第 i i i 个,然后可以选一个、两个、三个、四个……
1 2 3 4 5 6 7 for (int i=1 ;i<=n;i++) for (int j=0 ;j<=m;j++) { f[i][j]=f[i-1 ][j]; - if (j>=v[i]) f[i][j]=max (f[i][j],f[i-1 ][j-w[i]]+v[i]); + if (j>=v[i]) f[i][j]=max (f[i][j],f[i][j-w[i]]+v[i]); }
很简单,将滚动优化的第二维改为正向枚举即可,解释见上;
1 2 3 for (int i=1 ;i<=n;i++) for (int j=a[i];j<=m;j++) f[j]=max (f[j],f[j-w[i]]+v[i]);
等效替换:
求方案数状态表示集合:所有只从前 i i i 个物品中选,且总体积恰好是 j j j 的方案的集合。 属性:数量 状态计算根据第 i i i 个物品选几个来划分集合。 0 个:f i − 1 , j f_{i-1,j} f i − 1 , j k 个:f i − 1 , j − k × v i f_{i-1,j-k\times v_i} f i − 1 , j − k × v i f i , j = f i − 1 , j + f i − 1 , j − v i × k f_{i,j}=f_{i-1,j}+f_{i-1,j-v_i\times{k}} f i , j = f i − 1 , j + f i − 1 , j − v i × k
这里可以直接替换。
f i , j = f i − 1 , j + f i , j − v f_{i,j} = f_{i-1,j}+f_{i,j-v} f i , j = f i − 1 , j + f i , j − v
这是通过它们的层层递推关系得出的。
习题:
P1616
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <bits/stdc++.h> #define int long long using namespace std;const int N=1000 +5 ,M=100000 +5 ;int n,m,w[N],v[N],f[N][M];int main () { scanf ("%lld%lld" ,&m,&n); for (int i=1 ;i<=n;i++) scanf ("%lld%lld" ,&w[i],&v[i]); for (int i=1 ;i<=n;i++) for (int j=1 ;j<=m;j++) { if (j>=v[i]) f[i][j]=max (f[i-1 ][j],f[i][j-v[i]]+w[i]); else f[i][j]=f[i-1 ][j]; } return 0 ; }
有限背包P1776 宝物筛选
每次相当于求一个长度为 s s s 的窗口的最大值,并且有 w w w 的偏移量,这个可以通过一个单调队列来维护。
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 #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std;const int N = 100010 ;int n, m;int f[N], g[N], q[N];int main () { cin >> n >> m; for (int i = 0 ; i < n; i++) { int v, w, s; cin >> w >> v >> s; memcpy (g, f, sizeof (f)); for (int j = 0 ; j < v; j++) { int hh = 0 , tt = -1 ; for (int k = j; k <= m; k += v) { if (hh <= tt && q[hh] < k - s * v) hh++; if (hh <= tt) f[k] = max (f[k], g[q[hh]] + (k - q[hh]) / v * w); while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt--; q[++tt] = k; } } } cout << f[m] << endl; }
与无穷背包相比,每个物品选取次数有一个上限。
老样子我们可以通过枚举第三维来解决,或者加维来做无穷背包怎么优化呢?
那就可以把这些不同种物品全部摊开,变成一个一个的物品,这样就变成01背包了。
但这样还是 O ( n 3 ) O(n^3) O ( n 3 ) 的,我希望物品总数变少,可以把物品少拆一点,使得这个拆分满足选取若干数加起来可以等于 0 ∼ k 0\sim{k} 0 ∼ k 中的任何一个数。
这样不管选多少个物品都可以通过拆分的这些数组合得到。
比如这样,这样就可以把任何一个数拆分成 l o g n logn l o g n 个数,这样就把复杂度优化为 O ( n 2 l o g n ) O(n^2logn) O ( n 2 l o g n ) 。
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 #include <bits/stdc++.h> using namespace std;const int N = 1e5 + 7 ;int n, c;int w[N], v[N], m[N];int f[N];int cnt;int nw[N], nv[N];int main () { scanf ("%d%d" , &n, &c); for (int i = 1 ; i <= n; i++) { scanf ("%d%d%d" , &v[i], &w[i], &m[i]); } for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m[i]; j <<= 1 ) { m[i] -= j; nw[++cnt] = w[i] * j; nv[cnt] = v[i] * j; } if (m[i]) { nw[++cnt] = w[i] * m[i]; nv[cnt] = v[i] * m[i]; } } for (int i = 1 ; i <= cnt; i++) for (int j = c; j >= nw[i]; j--) { f[j] = max (f[j], f[j - nw[i]] + nv[i]); } cout << f[c] << endl; return 0 ; }
限制背包n n n 个物品,m m m 体积的背包, k k k 次询问,每次强制有一个物品不能选,问最大价值。n ≤ 1 0 5 , m ≤ 100 , k ≤ 1000 n\le{10^5},m\le{100},k\le{1000} n ≤ 1 0 5 , m ≤ 1 0 0 , k ≤ 1 0 0 0
我们注意 m m m 很小,这是一个切入点。
先正常 DP 一遍,f i , j f_{i,j} f i , j 表示前 i i i 个物品用了 j j j 的体积的最大价值。
再进行一次 DP,g i , j g_{i,j} g i , j 代表 i ∼ n i\sim{n} i ∼ n 已经考虑完了用了 j j j 的体积的最大价值。
所以查询 p 1 p_1 p 1 时,枚举体积 a , b a,b a , b ,答案就是 f p 1 − 1 , a + g p 1 + 1 , b f_{p_1-1,a}+g_{p_1+1,b} f p 1 − 1 , a + g p 1 + 1 , b 。
当前也可以分别维护一个前缀最大值,这查询的时候只需要枚举一个变量即可,如 a , m − a a,m-a a , m − a 。
背包 DP 的本质是不断往里加物品,在加物品的 DP 里考虑删物品很不好做,那不如分为两段累加 。
分组背包分组背包,通俗的讲就是,给你N N N 组物品,然后每一组你至多选择一个物品(也可以不选 ),每个物品都有自己的体积和价值,现在给你一个容里为M的背包,让你用这个背包装物品,使得物品价值总和最大。
那么就可以给它们编号了,然后定义一个数组来存储第i i i 组第j j j 个的编号,然后正常存储价值、重量等。
枚举时,第一维是组数,第二维是背包容量(从大到小),第三维是第i i i 组的物品数,然后判断下是否能转移。
1 f[j]=max (f[j],f[j-w[p[i][k]]]+v[p[i][k]]);
习题:
HDU1712
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 43 44 45 46 #include <bits/stdc++.h> using namespace std;const int N=13505 ;const int M=1007 ;int n,m;int x,w[N],v[N],f[N],p[M][M],c[N];int main () { while (1 ) { cin>>n>>m; if (!n&&!m) break ; memset (f,0 ,sizeof (f)); for (int i=1 ;i<=n;i++) for (int j=1 ;j<=m;j++) scanf ("%d" ,&p[i][j]); for (int i=1 ;i<=n;i++) for (int j=m;j>=0 ;j--) for (int k=1 ;k<=m;k++) if (j>=k) f[j]=max (f[j],f[j-k]+p[i][k]); cout<<f[m]<<'\n' ; } return 0 ; }
混合背包A7
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 #include <bits/stdc++.h> using namespace std;const int N = 1010 ;int n, m;int f[N];int main () { cin >> n >> m; for (int i = 0 ; i < n; i++) { int v, w, s; cin >> v >> w >> s; if (s == 0 ) { for (int j = v; j <= m; j++) f[j] = max (f[j], f[j - v] + w); } else { if (s == -1 ) s = 1 ; for (int k = 1 ; k <= s; k <<= 1 ) { for (int j = m; j >= k * v; j--) f[j] = max (f[j], f[j - k * v] + k * w); s -= k; } if (s) { for (int j = m; j >= s * v; j--) f[j] = max (f[j], f[j - s * v] + s * w); } } } cout << f[m] << endl; return 0 ; }
依赖背包A10
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 43 44 45 46 47 48 #include <bits/stdc++.h> using namespace std;const int N = 110 ;struct Edge { int nxt, to; }e[N]; int n, m, cnt;int v[N], w[N];int fir[N];int f[N][N];void add (int u, int v) { e[++cnt].to = v; e[cnt].nxt = fir[u]; fir[u] = cnt; } void dfs (int u) { for (int i = fir[u]; i; i = e[i].nxt) { int vv = e[i].to; dfs (vv); for (int j = m - v[u]; j >= 0 ; j--) for (int k = 0 ; k <= j; k++) f[u][j] = max (f[u][j], f[u][j - k] + f[vv][k]); } for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u]; for (int i = 0 ; i < v[u]; i++) f[u][i] = 0 ; } int main () { cin >> n >> m; int root; for (int i = 1 ; i <= n; i++) { int p; cin >> v[i] >> w[i] >> p; if (p == -1 ) root = i; else add (p, i); } dfs (root); cout << f[root][m] << endl; }
能量石问题分析方法类似国王游戏。
A734
吃第 i i i 块花费的时间是 s i s_i s i 秒,当时它的价值是 E i ′ E'_i E i ′ ,每秒它将失去 L i L_i L i 的价值。
那么我们在吃 i i i 和 i + 1 i+1 i + 1 的时候,考虑顺序。
假如先吃 i i i ,那么得到的价值就是 E i ′ + E i + 1 ′ − s i × L i + 1 E'_i+E'_{i+1}-s_i\times{L_{i+1}} E i ′ + E i + 1 ′ − s i × L i + 1
假如先吃 i + 1 i + 1 i + 1 ,那么得到的价值就是 E i ′ + E i + 1 ′ − s i + 1 × L i E'_i+E'_{i+1}-s_{i+1}\times{L_i} E i ′ + E i + 1 ′ − s i + 1 × L i
我们发现只有最后一项不同。
假如先吃 i i i 更优,那么就说明 s i × L i + 1 < s i + 1 × L i s_i\times{L_{i+1}}<s_{i+1}\times{L_i} s i × L i + 1 < s i + 1 × L i
所以我们可以按照 s i L i \frac{s_i}{L_i} L i s i 从小到大排序,依次选即可。
状态表示f i , j f_{i,j} f i , j 集合:所有只从前 i i i 块能量石中选,且总体积(花费的时间)恰好是 j j j 的方案 属性:最大值 状态计算f i , j = m a x ( f i − 1 , j , f i − 1 , j − s i + E i − ( j − s i ) × L i ) f_{i,j}=max(f_{i-1,j},f_{i-1,j-s_i}+E_i-(j-s_i)\times{L_i}) f i , j = m a x ( f i − 1 , j , f i − 1 , j − s i + E i − ( j − s i ) × L i ) 注意这里由于要通过当前时间来计算能量石损耗了多少能量,所以我们的 j j j 记录的是恰好为当前时间的状态。
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 43 44 45 46 47 48 49 #include <bits/stdc++.h> using namespace std;const int N = 10010 ;int n;struct Stone { int s, e, l; bool operator < (const Stone &t) const { return s * t.l < t.s * l; } }stone[N]; int f[N];int main () { int t; cin >> t; for (int C = 1 ; C <= t; C++) { int m = 0 ; cin >> n; for (int i = 0 ; i < n; i++) { int s, e, l; cin >> s >> e >> l; m += s; stone[i] = {s, e, l}; } sort (stone, stone + n); memset (f, 0xc0 , sizeof (f)); f[0 ] = 0 ; for (int i = 0 ; i < n; i++) { int s = stone[i].s, e = stone[i].e, l = stone[i].l; for (int j = m; j >= s; j--) { f[j] = max (f[j], f[j - s] + e - (j - s) * l); } } int res = 0 ; for (int i = 0 ; i <= m; i++) res = max (res, f[i]); printf ("Case #%d: %d\n" , C, res); } return 0 ; }
状态机模型描述的是一个过程而非结果。
把点扩展成了一个过程。
A1049
f i f_i f i 表示抢劫前 i i i 家店铺的最大收益。
正常转移:f i = max ( f i − 1 , f i − 2 + w i ) f_i=\max(f_{i-1},f_{i-2}+w_i) f i = max ( f i − 1 , f i − 2 + w i )
状态机分析:
先拆解状态为:f i , 0 , f i , 1 f_{i,0},f_{i,1} f i , 0 , f 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 #include <bits/stdc++.h> using namespace std;const int N = 1e5 + 8 , INF = 0x3f3f3f3f ;int n;int w[N], f[N][2 ];int main () { int t; cin >> t; while (t--) { cin >> n; f[0 ][0 ] = 0 , f[0 ][1 ] = -INF; for (int i = 1 ; i <= n; i++) { scanf ("%d" , &w[i]); f[i][0 ] = max (f[i - 1 ][0 ], f[i - 1 ][1 ]); f[i][1 ] = f[i - 1 ][0 ] + w[i]; } cout << max (f[n][0 ], f[n][1 ]) << endl; } return 0 ; }
A1057
对于股票对应两个过程:手中有货,手中无货。
状态表示:f i , j , 0 , f i , j , 1 f_{i, j, 0}, f_{i, j, 1} f i , j , 0 , f i , j , 1 集合:过完了前 i i i 天,进行了 j j j 次交易,并且手中没有/有股票的状态。 假如手中有股票,那么就是处于第 j j j 次交易。 状态计算:f i , j , 0 = max ( f i − 1 , j , 0 , f i − 1 , j , 1 + w i ) f_{i,j,0}=\max(f_{i-1,j,0},f_{i-1,j,1}+w_i) f i , j , 0 = max ( f i − 1 , j , 0 , f i − 1 , j , 1 + w i ) f i , j , 1 = max ( f i − 1 , j , 1 , f i − 1 , j − 1 , 0 − w i ) f_{i,j,1}=\max(f_{i-1,j,1},f_{i-1,j-1,0}-w_i) f i , j , 1 = max ( f i − 1 , j , 1 , f i − 1 , j − 1 , 0 − w i ) 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 #include <bits/stdc++.h> using namespace std;const int N = 100010 , M = 110 ;int n, m;int w[N];int f[N][M][2 ];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) scanf ("%d" , &w[i]); memset (f, 0xc0 , sizeof (f)); for (int i = 0 ; i <= n; i++) f[i][0 ][0 ] = 0 ; for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) { f[i][j][0 ] = max (f[i - 1 ][j][0 ], f[i - 1 ][j][1 ] + w[i]); f[i][j][1 ] = max (f[i - 1 ][j][1 ], f[i - 1 ][j - 1 ][0 ] - w[i]); } int res = 0 ; for (int i = 1 ; i <= m; i++) res = max (res, f[n][i][0 ]); cout << res << endl; return 0 ; }
A1058
状态表示手中有货/手中无货的第一天/手中无货的非第一天。 入口:手中无货的非第一天。 出口:手中无货即可,那么算一个即可,但假如股票价格是单调下降的,那么最优解就是一次也不买,出口就是入口。 注意本题无交易次数。 状态计算:f i , 0 / 1 / 2 f_{i,0/1/2} f i , 0 / 1 / 2 f i , 0 = max ( f i − 1 , 0 , f i − 1 , 2 − w i ) f_{i,0}=\max(f_{i-1,0},f_{i-1,2}-w_i) f i , 0 = max ( f i − 1 , 0 , f i − 1 , 2 − w i ) f i , 1 = f i − 1 , 0 + w i f_{i,1}=f_{i-1,0}+w_i f i , 1 = f i − 1 , 0 + w i f i , 2 = max ( f i − 1 , 2 , f i − 1 , 1 ) f_{i,2}=\max(f_{i-1,2},f_{i-1,1}) f i , 2 = max ( f i − 1 , 2 , f i − 1 , 1 ) 初始化:f 0 , 2 = 0 , f 0 , 1 = f 0 , 0 = − ∞ f_{0,2}=0,f_{0,1}=f_{0,0}=-\infty f 0 , 2 = 0 , f 0 , 1 = f 0 , 0 = − ∞ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <bits/stdc++.h> using namespace std;const int N = 100010 ;int n;int w[N];int f[N][3 ];int main () { cin >> n; for (int i = 1 ; i <= n; i++) cin >> w[i]; f[0 ][0 ] = f[0 ][1 ] = -0x3f3f3f3f ; f[0 ][2 ] = 0 ; for (int i = 1 ; i <= n; i++) { f[i][0 ] = max (f[i - 1 ][0 ], f[i - 1 ][2 ] - w[i]); f[i][1 ] = f[i - 1 ][0 ] + w[i]; f[i][2 ] = max (f[i - 1 ][1 ], f[i - 1 ][2 ]); } cout << max (f[n][1 ], f[n][2 ]) << endl; return 0 ; }
A1052
KMP 的过程中不能让它跳到最后一个状态。
每个位置有 26 条边,m + 1 m + 1 m + 1 个状态。
1:40:00 没听懂,复习 KMP 去了,等回来补听。 状压DP在背包问题里,考虑的顺序是对答案没有影响的。 但是会存在某些题,物品的选择顺序对答案产生影响。 比如这次获得的实际价值是这次选择的物品价值与上一个物品价值异或和,那么选择顺序就会导致答案变化 ,就需要一种新的DP方式——状压DP。
状压DP的暴力做法一般是 n ! n! n ! 复杂度的,比如枚举选择顺序的全排列。
设 f i , j f_{i,j} f i , j 代表上一个选的物品是 j j j ,第一维用一个 n n n 位的二进制数(0 ∼ n − 1 0\sim{n-1} 0 ∼ n − 1 ),这个二进制数第 x x x 位代表第 x x x 个物品选没选。
转移的时候看一下第 i − 1 i-1 i − 1 位有没有选,没选我才能选上。
用二进制数代表物品选没选过,这个技巧叫做状态压缩。
还要考虑体积:当前用了 k k k 的体积。
f i , j , k f_{i,j,k} f i , j , k 代表选了哪些,上一个选的是 j j j ,当前用了 k k k 的体积所得到的最大价值,转移时考虑下一个选什么,编号 0 ≤ r ≤ n 0\le{r}\le{n} 0 ≤ r ≤ n 。
选这个物品的条件是( ( i > > r ) & 1 ) = 0 ((i>>r)\&1)=0 ( ( i > > r ) & 1 ) = 0
f i ∣ ( 1 < < r ) , r , k + v r = f i , j , k + w j ⊕ w r f_{i|(1<<r),r,k+v_r}=f_{i,j,k}+w_j\oplus{w_r} f i ∣ ( 1 < < r ) , r , k + v r = f i , j , k + w j ⊕ w r
i ∣ ( 1 < < r ) i|(1<<r) i ∣ ( 1 < < r ) 相当于把 i i i 的第 r r r 位赋为 1。
复杂度:
O ( 2 n × n 2 × m ) O(2^n\times{n^2}\times{m}) O ( 2 n × n 2 × m )
这个范围一般是 n ∈ 18 ∼ 22 n\in{18\sim{22}} n ∈ 1 8 ∼ 2 2
棋盘式(基于连通性)A1064
状态表示f i , j , s f_{i,j,s} f i , j , s 集合:所有只摆在前 i i i 行,已经摆了 j j j 个国王,且第 i i i 行摆放的状态是 s s s 的集合。 属性:方案数。 状态计算已经摆完前 i i i 排,且第 i i i 排的状态是 a a a ,第 i − 1 i - 1 i − 1 排的状态是 b b b ,已经摆了 j j j 个国王的所有方案。 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <cstring> #include <iostream> #include <algorithm> #include <vector> using namespace std;const int N = 12 , M = 1 << 10 , K = 110 ;typedef long long LL;int n, m;vector<int > state; int id[M];vector<int > head[M]; int cnt[M];LL f[N][K][M]; bool check (int state) { for (int i = 0 ; i < n; i++) if (((state >> i) & 1 ) && (state >> (i + 1 )) & 1 ) return false ; return true ; } int count (int state) { int res = 0 ; for (int i = 0 ; i < n; i++) res += (state >> i) & 1 ; return res; } int main () { cin >> n >> m; for (int i = 0 ; i < (1 << n); i++) if (check (i)) { state.push_back (i); id[i] = state.size () - 1 ; cnt[i] = __builtin_popcount(i); } for (int i = 0 ; i < state.size (); i++) for (int j = 0 ; j < state.size (); j++) { int a = state[i], b = state[j]; if ((a & b) == 0 && check (a | b)) head[i].push_back (j); } f[0 ][0 ][0 ] = 1 ; for (int i = 1 ; i <= n + 1 ; i++) for (int j = 0 ; j <= m; j++) for (int a = 0 ; a < state.size (); a++) for (int b : head[a]) { int c = cnt[state[a]]; if (j >= c) { f[i][j][a] += f[i - 1 ][j - c][b]; } } cout << f[n + 1 ][m][0 ] << endl; return 0 ; }
A327
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #include <iostream> using namespace std;const int MOD = 1e8 ;int n, m;int can[15 ][15 ];int f[15 ][1 << 12 ];int ans;bool check (int x, int s) { if (s & (s << 1 ) || s & (s >> 1 )) return false ; for (int i = 1 ; i <= m; i++) if (((s >> (i - 1 )) & 1 ) && can[x][i] == 0 ) return false ; return true ; } int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) cin >> can[i][j]; f[0 ][0 ] = 1 ; for (int i = 1 ; i <= n + 1 ; i++) for (int j = 0 ; j < (1 << m); j++) if (check (i - 1 , j)) for (int k = 0 ; k < (1 << m); k++) if ((j & k) == 0 && check (i, k)) f[i][k] = (f[i][k] + f[i - 1 ][j]) % MOD; cout << f[n + 1 ][0 ] << endl; return 0 ; }
旅行商问题(TSP)平面内有 n n n 个点,问从 1 1 1 号点出发把所有点都走一遍回到自己的最短路径。
除了一号点,其它的点没有必要走两次。
设 f i , j f_{i,j} f i , j 代表哪些点走过了,最后停留在 j j j 号点的最短路径。
初始化为 f 1 , 0 = 0 f_{1,0}=0 f 1 , 0 = 0 ,把编号改为 0 ∼ n − 1 0\sim{n-1} 0 ∼ n − 1 。
然后枚举一个 k k k ,表示最后停在 j j j ,这一步是从 j j j 走到 k k k 。
所以 f i ∣ ( 1 < < k ) , k = min ( f i , j + d i s j , k ) f_{i|(1<<k),k}=\min(f_{i,j}+dis_{j,k}) f i ∣ ( 1 < < k ) , k = min ( f i , j + d i s j , k )
一共有 n n n 个点,预处理出 n 2 n^2 n 2 条抛物线和它们覆盖的点。
问题就转化为了给定若干条抛物线,用最少的抛物线覆盖所有的点。
这是一个重复覆盖问题 ,与之相对的还有精确覆盖问题 ,最优解法是 Dancing Links。
所以这题用状压实现一下。
用集合类型的状压 DP 优化爆搜。
爆搜:
如果包含了所有列,更新全局最优解。 s t a t e state s t a t e 存储哪一列已经被覆盖任选没有被覆盖的一列 x x x 枚举所有能覆盖 x x x 的抛物线。 更新 s t a t e state s t a t e 区间DP 结构它是由小区间递推得到大区间的一种 DP 方式,可以先枚举长度,再枚举左端点得到右端点,也可以直接枚举左右端点。
但之间枚举左右端点要注意顺序问题,即大的左端点要先被枚举到:
1 2 for (int i = n; i >= 1 ; i--) for (int j = i; j <= n; j++)
这样可以保证枚举到的区间的子区间之前一定更新过。
不过个人还是比较习惯:
1 2 3 4 5 for (int len = 1 ; len <= n; len++) for (int l = 1 ; l + len - 1 <= n; l++) { int r = l + len - 1 ; }
合并石子有 n n n 堆石子,每次选择相邻两堆合并,合并代价为两堆石子之和,要合并为一堆,求最小代价。
它始终是连续的一段进行合并,不能进行跳跃。
所以可以设计 f l , r f_{l,r} f l , r 代表把 [ l , r ] [l,r] [ l , r ] 的石子合并为一堆的最小代价,答案就是 f 1 , n f_{1,n} f 1 , n 。
初始化为 f i , i = 0 f_{i,i}=0 f i , i = 0 ,那么怎么进行转移?
最后一次合并一定是把两堆石子合并为一堆石子,所以两堆石子之间一定有一个分界点,所以就枚举一个分界点 k k k ,f l , r = min l ≤ k ≤ r ( f l , k + f k + 1 , r ) + s u m l , r f_{l,r}=\min_{l\le{k}\le{r}}(f_{l,k}+f{k+1,r})+sum_{l,r} f l , r = min l ≤ k ≤ r ( f l , k + f k + 1 , r ) + s u m l , r 。
区间DP的共性之一:状态有 l l l 和 r r r 。
大区间一定是由小区间转移而来的,所以我们要先求长度较小的区间,因此不能直接枚举 l , r l,r l , r ,要枚举区间长度 。
先枚举区间长度,再枚举 l , r l,r l , r ,再枚举断点。
石子合并(弱化版)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <bits/stdc++.h> using namespace std;int n;int a[10000 ];int f[1000 ][1000 ];int sum[10000 ];int main () { scanf ("%d" ,&n); for (int i=1 ;i<=n;i++) scanf ("%d" ,&a[i]); for (int i=1 ;i<=n;i++) sum[i]=sum[i-1 ]+a[i]; memset (f,0x3f3f3f ,sizeof (f)); for (int i=1 ;i<=n;i++) f[i][i]=0 ; for (int len=2 ;len<=n;len++) for (int l=1 ,r=len;r<=n;r++,l++) for (int k=l;k<r;k++) f[l][r]=min (sum[r]-sum[l-1 ]+f[l][k]+f[k+1 ][r],f[l][r]); printf ("%d" ,f[1 ][n]); return 0 ; }
那么石子是环呢?
我们合并为一堆时,总会有一个地方是分界点,可以断环为链,枚举每一个分界点的答案即可。
石子合并
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 #include <bits/stdc++.h> using namespace std;int n;int a[10000 ];int f[1000 ][1000 ];int sum[10000 ];int main () { scanf ("%d" ,&n); for (int i=1 ;i<=n;i++) scanf ("%d" ,&a[i]); for (int i=n+1 ;i<=2 *n;i++) a[i]=a[i-n]; for (int i=1 ;i<=2 *n;i++) sum[i]=sum[i-1 ]+a[i]; memset (f,0x3f ,sizeof (f)); for (int i=1 ;i<=2 *n;i++) f[i][i]=0 ; for (int len=2 ;len<=n;len++) for (int l=1 ,r=len;r<=2 *n;r++,l++) for (int k=l;k<r;k++) f[l][r]=min (sum[r]-sum[l-1 ]+f[l][k]+f[k+1 ][r],f[l][r]); int ans=f[1 ][n]; for (int l=1 ,r=n;r<=2 *n;l++,r++) ans=min (ans,f[l][r]); int maxn,minn; minn=ans; for (int len=2 ;len<=n;len++) for (int l=1 ,r=len;r<=2 *n;r++,l++) for (int k=l;k<r;k++) f[l][r]=max (sum[r]-sum[l-1 ]+f[l][k]+f[k+1 ][r],f[l][r]); ans=f[1 ][n]; for (int l=1 ,r=n;r<=2 *n;l++,r++) ans=max (ans,f[l][r]); maxn=ans; printf ("%d\n%d" ,minn,maxn); return 0 ; }
矩阵乘法分别有一堆矩阵 M 1 = a 1 × a 2 , M 2 = a 2 × a 3 , M 3 = a 3 × a 4 , ⋯ , M n = a n × a n + 1 M_1={a_1}\times{a_2},M_2={a_2}\times{a_3},M_3={a_3}\times{a_4},\cdots,M_n={a_n}\times{a_{n+1}} M 1 = a 1 × a 2 , M 2 = a 2 × a 3 , M 3 = a 3 × a 4 , ⋯ , M n = a n × a n + 1 。现在要把它们乘起来,两个矩阵相乘的代价,例如M 1 ⋅ M 2 = a 1 × a 2 × a 3 M_1\cdot{M_2}=a_1\times{a_2}\times{a_3} M 1 ⋅ M 2 = a 1 × a 2 × a 3 ,根据矩阵的结合律,求最小代价。
类似于能量项链 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <bits/stdc++.h> using namespace std;const int N=220 ;int n;int a[N];int f[N][N];int main () { scanf ("%d" ,&n); for (int i=1 ;i<=n;i++) scanf ("%d" ,&a[i]); for (int i=n+1 ;i<=2 *n;i++) a[i]=a[i-n]; for (int len=2 ;len<=n*2 ;len++) for (int l=1 ,r=len;r<=n*2 ;l++,r++) for (int k=l;k<r;k++) f[l][r]=max (f[l][r],f[l][k]+f[k+1 ][r]+a[l]*a[k+1 ]*a[r+1 ]); int ans=-1 ; for (int i=1 ;i<=n;i++) ans=max (ans,f[i][i+n-1 ]); printf ("%d" ,ans); return 0 ; }
POJ2955 最长括号匹配从一个括号串中选出一个最长的子序列,使其满足括号匹配。
f l , r f_{l,r} f l , r 代表 [ l , r ] [l,r] [ l , r ] 中最多能选出多少括号满足匹配。
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 #include <iostream> #include <cstdio> #include <string.h> #include <cstring> #include <string> using namespace std;const int N=300 ;char a[N];int f[N][N];string s; int main () { while (true ) { scanf ("%s" ,a); s=a; if (s=="end" ) return 0 ; int n=s.length (); memset (f,0 ,sizeof (f)); for (int len=2 ;len<=n;len++) { for (int l=0 ,r=len-1 ;r<n;l++,r++) { for (int k=l;k<r;k++) f[l][r]=max (f[l][r],f[l][k]+f[k+1 ][r]); if (s[l]=='(' &&s[r]==')' ) f[l][r]=max (f[l][r],f[l+1 ][r-1 ]+2 ); if (s[l]=='[' &&s[r]==']' ) f[l][r]=max (f[l][r],f[l+1 ][r-1 ]+2 ); } } printf ("%d\n" ,f[0 ][n-1 ]); } return 0 ; }
POJ1651 删数
还是用 f l , r f_{l,r} f l , r 表示删除区间 [ l , r ] [l,r] [ l , r ] (除左右端点 )的总代价,答案就是 f 1 , n f_{1,n} f 1 , n 。
转移呢?
删除最后一个数 k k k ,拿肯定是先把 l ∼ k l\sim{k} l ∼ k 和 k ∼ r k\sim{r} k ∼ r 之间的点都删光,
枚举一个中间点,f l , r = min l < k < r ( f l , k + f k , r + a l × a k × a r ) f_{l,r}=\min_{l<k<r}(f_{l,k}+f_{k,r}+a_l\times{a_k}\times{a_r}) f l , r = min l < k < r ( f l , k + f k , r + a l × a k × a r )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include <cstring> using namespace std;const int N=200 ;int a[N],f[N][N];int n;int main () { scanf ("%d" ,&n); for (int i=1 ;i<=n;i++) scanf ("%d" ,&a[i]); memset (f,0x3f ,sizeof (f)); for (int i=1 ;i<n;i++) f[i][i+1 ]=0 ; for (int len=3 ;len<=n;len++) for (int l=1 ,r=len;r<=n;l++,r++) { for (int k=l+1 ;k<r;k++) f[l][r]=min (f[l][r],f[l][k]+f[k][r]+a[l]*a[k]*a[r]); } printf ("%d" ,f[1 ][n]); return 0 ; }
HDU4632 回文子序列给定一个字符串,求回文子序列的数量。
对于一个区间 [ l , r ] [l,r] [ l , r ] ,我在处理它的时候一定要保证它的子区间已经处理完了。
转移:f l , r = f l + 1 , r + f l , r − 1 − f l + 1 , r − 1 f_{l,r}=f_{l+1,r}+f_{l,r-1}-f_{l+1,r-1} f l , r = f l + 1 , r + f l , r − 1 − f l + 1 , r − 1
这就完了嘛?如果 s l = s r s_l=s_r s l = s r ,那么就是在原来 [ l , r ] [l,r] [ l , r ] 的基础上又加了这么多,所以要再加上 f l + 1 , r − 1 f_{l+1,r-1} f l + 1 , r − 1 ,而且两端点也形成了一个回文子序列,所以再加一。
于是总转移方程为:f l , r = f l , r − 1 + f l + 1 , r − [ s l ≠ s r ] × f l + 1 , r − 1 f_{l,r}=f_{l,r-1}+f_{l+1,r}-[s_l\not=s_r]\times{f_{l+1,r-1}} f l , r = f l , r − 1 + f l + 1 , r − [ s l = s r ] × f l + 1 , r − 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 #include <bits/stdc++.h> using namespace std;const int mod=10007 ;const int N=1050 ;int t,n;char s[N];int f[N][N];int main () { scanf ("%d" ,&t); for (int k=1 ;k<=t;k++) { scanf ("%s" ,s+1 ); n=strlen (s+1 ); memset (f,0 ,sizeof (f)); for (int i=1 ;i<=n;i++) f[i][i]=1 ; for (int len=2 ;len<=n;len++) for (int l=1 ,r=len;r<=n;l++,r++) f[l][r]=(f[l][r-1 ]+f[l+1 ][r]-((int )(s[l]!=s[r])*(f[l+1 ][r-1 ]))+(int )(s[l]==s[r])+mod)%mod; printf ("Case %d: %d\n" ,k,f[1 ][n]); } return 0 ; }
凸多边形
一个 n n n 边形是可以切成 n − 2 n-2 n − 2 个三角形,每个三角形的权值是它的三个顶点的点权之积,求最小权值和。
三角形是通过切割得来的。
断环为链,拷贝几份,假如一个凸八边形,那么f 1 , 9 = f 1 , 5 + f 5 , 9 f_{1,9}=f_{1,5}+f_{5,9} f 1 , 9 = f 1 , 5 + f 5 , 9 ,其中 a 1 = a 9 a_1=a_9 a 1 = a 9 。
f i , j = f i , k + f k , j f_{i,j}=f_{i,k}+f_{k,j} f i , j = f i , k + f k , j
当这个区间被划分到三个点的时候单独算一下就好了。
树形DP始终从叶子向根节点做 DP。
在每个节点时聚合所有儿子的信息,可能需要多遍 dfs/bfs 。
点数一个 n n n 个点的树,问这个树有多少个点?
很显然是n。
考虑用树形 DP 来解决这个问题。
树形DP的第一个维度一般是 f i f_i f i ,代表以 i i i 为根的子树。
在这里表示以 i i i 为根的子树有多少个点。
所以我们要求的就是 f 1 f_1 f 1 。
初始化?我们需要把所有叶子节点的子树大小初始化为 1 1 1 。
初始化的一般是叶子节点
转移?由儿子向父亲转移,把所有儿子的信息合在一起 。
f u = ∑ f v + 1 f_u=\sum{f_v}+1 f u = ∑ f v + 1
树形 DP 的时候我们一般会进行 DFS,同时记录当前点和它的父亲。
1 2 3 4 5 6 7 8 9 10 void dfs (int u,int fa) { f[u]=1 ; for (int i=fir[u];i;i=e[i].nxt) if (e[i].to!=fa) { dfs (e[i].to,u); f[u]+=f[v]; } }
直径求树的直径,直径定义为树上两个最远距离的点的路径。
树的直径无非是树上的两条链,而树上的一条链我们可以怎样表示?
假设从 p 1 p_1 p 1 走到 p 2 p_2 p 2 ,那肯定是先走到它们的 l c a lca l c a 再走到另一个点。
而对于一个点我只需要向下找两条尽量长的路径,拼起来就是以这个点为拐点的最长路径,这样对每个点的最长路径取一个 m a x max m a x 就可以了。
那么怎么对每一个点都找到这样一条最长路径呢?
求以 i i i 点出发向下的最长路和次长路,分别用 f i f_i f i 和 g i g_i g i 来表示。
对于叶子节点,两个量都是 0。
转移?
f i = m a x ( f p j ) + 1 f_i=max(f_{p_j})+1 f i = m a x ( f p j ) + 1 ,g i g_i g i 呢?,假如先走了 p 3 p_3 p 3 ,那么次长路就一定不能是 p 3 p_3 p 3 因为这样就把 i ↔ p 3 i\leftrightarrow{p_3} i ↔ p 3 走了两遍,所以 g i = s e c o n d _ m a x ( f p j ) + 1 g_i=second\_max(f_{p_j})+1 g i = s e c o n d _ m a x ( f p j ) + 1
最后的答案为 m a x ( f i + g i ) max(f_i+g_i) m a x ( f i + g i )
复杂度为 O ( n ) O(n) O ( n )
路径求树上所有路径长度和
f i f_i f i 表示以 i i i 为根节点的子树的路径和,叶子节点为0,转移?没法转移,因为会有跨越不同子树的路径,还要维护一个 g i g_i g i 表示所有点到根节点的距离之和。
g p 1 + s i z p 1 g_{p_1}+siz_{p_1} g p 1 + s i z p 1 就是所有点到 i i i 的路径长度之和,它们到其它子树也要算上,也就是 ( g p 1 + s i z p 1 ) × ( s i z i − s i z p 1 ) (g_{p_1}+siz_{p_1})\times(siz_i-siz_{p_1}) ( g p 1 + s i z p 1 ) × ( s i z i − s i z p 1 ) ,因为这跳路径会被算这么多次。
所以:
f i = ∑ j ∈ s o n i f p j + ( g p j + s i z p j × v a l i ↔ j ) × ( s i z i − s i z p j ) f_i=\sum_{j\in{son_i}}f_{p_j}+(g_{p_j}+siz_{p_j}\times{val_{i\leftrightarrow{j}}})\times(siz_i-siz_{p_j}) f i = j ∈ s o n i ∑ f p j + ( g p j + s i z p j × v a l i ↔ j ) × ( s i z i − s i z p j )
但是这样太复杂了,怎么优化呢?
其实我们只需要 DP 一个东西: s i z i siz_i s i z i
这个就已经足够了。
答案式:
∑ i = 2 n s i z i × n − s i z i × 2 \sum_{i=2}^n{siz_i\times{n-siz_i}\times{2}} i = 2 ∑ n s i z i × n − s i z i × 2
刚才我们是以点的角度考虑的,不妨换一种角度,考虑每条边。
考虑有多少条路径包含了这条边,这条边的左边有一个子树,在这个子树内任意一个点出发,以这个子树外任意一点为终点,都会经过这条边,而起点和终点又可以交换,所以是这些。注意不包含根节点,因为我们实际考虑的是边的贡献 。
树的最大独立集问一个树最多能取出多少个点使得这些点都不相邻。
状态为 f i , 0 / 1 f_{i,0/1} f i , 0 / 1 代表第 i i i 个点选/不选时子树最大独立集的大小。
初始化的话就是对于每个叶子节点,f i , 0 = 0 , f i , 1 = 1 f_{i,0}=0,f_{i,1}=1 f i , 0 = 0 , f i , 1 = 1
那么转移呢?
如果 i i i 这个点选了,那么它所有的儿子都不能选,也就是:
f i , 1 = ∑ j ∈ s o n i f j , 0 + 1 f_{i,1}=\sum_{j\in{son_i}}f_{j,0}+1 f i , 1 = ∑ j ∈ s o n i f j , 0 + 1
不选就无所谓了
f i , 0 = ∑ j ∈ s o n i max ( f j , 0 , f j , 1 ) f_{i,0}=\sum_{j\in{son_i}}\max(f_{j,0},f_{j,1}) f i , 0 = ∑ j ∈ s o n i max ( f j , 0 , f j , 1 )
士兵在一棵树上布置士兵,每个士兵在节点上,每个士兵可以守护与其相连的点,问最少需要多少个士兵。
第一维还是指以 i i i 为根的子树,这个根节点有三种情况,一个是自己守护自己,一个是它的儿子守护它,一个是它的父亲守护它。
0 − s o n , 1 − s e l f , 2 − f a 0-son,1-self,2-fa 0 − s o n , 1 − s e l f , 2 − f a
对于叶子节点,f i , 0 = i n f , f i , 1 = 1 , f i , 2 = 0 f_{i,0}=inf,f_{i,1}=1,f_{i,2}=0 f i , 0 = i n f , f i , 1 = 1 , f i , 2 = 0
转移?
f i , 0 = ∑ j ∈ s o n i f i , 1 = ∑ j ∈ s o n i min ( f j , 0 , f j , 1 , f j , 2 ) + 1 f i , 2 = ∑ j ∈ s o n i f j , 0 \begin{aligned} f_{i,0}&=\sum_{j\in{son_i}}\\ f_{i,1}&=\sum_{j\in{son_i}}\min(f_{j,0},f_{j,1},f_{j,2})+1\\ f_{i,2}&=\sum_{j\in{son_i}}f_{j,0}\\ \end{aligned} f i , 0 f i , 1 f i , 2 = j ∈ s o n i ∑ = j ∈ s o n i ∑ min ( f j , 0 , f j , 1 , f j , 2 ) + 1 = j ∈ s o n i ∑ f j , 0
被父亲守护时,不能被儿子守护,因为这样会重复计算,同理在计算被儿子守护时不能计算被父亲守护。
被儿子守护时,至少一个儿子要有士兵,常见技巧:聚合儿子时,需要用另外一个 DP 来维护转移。
设 g k , 0 / 1 g_{k,0/1} g k , 0 / 1 代表已经考虑完了前 k k k 个儿子,是否至少有一个儿子放了士兵的所得到的最小士兵数量。
转移:
g k , 0 = g k − 1 , 0 + f p k , 0 g_{k,0}=g_{k-1,0}+f_{p_k,0} g k , 0 = g k − 1 , 0 + f p k , 0
g k , 1 = min ( g k − 1 , 0 + f p k , 1 , g k − 1 , 1 + f p k , 0 , g k − 1 , 1 + f p k , 1 ) g_{k,1}=\min(g_{k-1,0}+f_{p_k,1},g_{k-1,1}+f_{p_k,0},g_{k-1,1}+f_{p_k,1}) g k , 1 = min ( g k − 1 , 0 + f p k , 1 , g k − 1 , 1 + f p k , 0 , g k − 1 , 1 + f p k , 1 )
这个 DP 就是为了聚合转移 f i , 0 f_{i,0} f i , 0 的。
假如 i i i 有 r r r 个儿子,那么 f i , 0 = g r , 1 f_{i,0}=g_{r,1} f i , 0 = g r , 1
树形DP的时候常用DP来做聚合。
依赖背包树形背包 n n n 个物品彼此组成一个树,如果想选第 i i i 种物品,必须先选它的父亲,问获得的最大价值。
状态:f i , j f_{i,j} f i , j 前 i i i 种物品用了 j j j 的体积所得到的最大价值 → \to → 在 i i i 的子树中选一些物品用了 j j j 的体积所能获得的最大价值。
初始化: 叶子节点 f i , 0 = 0 , f i , w i = v i , f i , j = − i n f f_{i,0}=0,f_{i,w_i}=v_i,f_{i,j}=-inf f i , 0 = 0 , f i , w i = v i , f i , j = − i n f
转移: 聚合的时候就要再做一个背包:
g i , j g_{i,j} g i , j 代表前 i i i 个儿子的子树已经用掉了 j j j 的体积得到的最大价值,g 0 , 0 = 0 , g 0 , j = − i n f g_{0,0}=0,g_{0,j}=-inf g 0 , 0 = 0 , g 0 , j = − i n f ,转移就枚举一下第 i i i 个儿子用掉了多少个体积:
g i , j = max 0 ≤ k ≤ j ( g i − 1 , j − k + f i , k ) g_{i,j}=\max_{0\le{k}\le{j}}(g_{i-1,j-k}+f_{i,k}) g i , j = 0 ≤ k ≤ j max ( g i − 1 , j − k + f i , k )
f i , 0 = 0 , f i , j = g r , j − v i + w i f_{i,0}=0,f_{i,j}=g_{r,j-v_i}+w_i f i , 0 = 0 , f i , j = g r , j − v i + w i
所以这是一个不断用 f f f 求 g g g ,再用 g g g 求 f f f 的做法。
复杂度为 O ( n × m 2 ) O(n\times{m^2}) O ( n × m 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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <bits/stdc++.h> using namespace std;const int N=305 ;int n,m,edcnt;int f[N][N];int s[N];int fir[N];struct edge { int to,nxt; }e[N]; void add (int u,int v) { e[++edcnt].to=v; e[edcnt].nxt=fir[u]; fir[u]=edcnt; } void dfs (int u) { for (int i=fir[u];i;i=e[i].nxt) { int v=e[i].to; dfs (v); for (int j=m+1 ;j>=1 ;j--) for (int k=0 ;k<j;k++) f[u][j]=max (f[u][j],f[v][k]+f[u][j-k]); } } int main () { scanf ("%d%d" ,&n,&m); for (int i=1 ;i<=n;i++) { int k; scanf ("%d%d" ,&k,&s[i]); f[i][1 ]=s[i]; add (k,i); } dfs (0 ); printf ("%d" ,f[0 ][m+1 ]); return 0 ; }
数位DP DP 方式参考文章 迭代 or 记搜?
记搜!
不要写迭代,又难想又难调。
常用形参:
设状态 f p o s , s u m f_{pos,sum} f p o s , s u m 代表位置 [ p o s + 1 , l e n ] [pos+1,len] [ p o s + 1 , l e n ] 已经填完了,这些数位之和在 s u m sum s u m 的情况下,数位 [ 1 , p o s ] [1,pos] [ 1 , p o s ] 无 l i m i t limit l i m i t 的情况下,所有数的数位之和。
l a s t last l a s t 记录上一位选的数,通常是为了限制当前位。l e a d lead l e a d 是否有前导零。r r r 余数s t st s t 状态压缩一般递归树中只有叶子节点存储的是该节点状态的答案,其它节点都是累加了它以及它子树的状态的答案。
而且初始状态已经以形参的形式传入到函数当中,无需费心初始化。
dp \text{dp} dp 数组的下标表示的是一种状态,独一无二的状态,一个状态是否要记录取决于它是否足够特殊,是否对答案有影响。
而有限制和无限制对应的状态又不同,因此当限制对答案数会产生影响时,要先判断没有限制,再记忆答案。
如何判断记忆化的值能否复用?
看下每次递归,影响答案的参数是否有改变?
是否是在非限制条件下记录的答案?
是否是在没有前导零的条件下记录的答案?
是否模数不同?
都要考虑。
数的个数举个例子:给你两个整数 l , r l,r l , r ,问 [ l , r ] [l,r] [ l , r ] 有多少个数,显然答案是 r − l + 1 r-l+1 r − l + 1 ,那么怎么用数位DP来解决,数位DP的特征一般为 [ l , r ] [l,r] [ l , r ] 中满足某种条件的数有多少个。
那就可以转化为 [ 0 , r ] [0,r] [ 0 , r ] 中满足条件的数的个数减去 [ 0 , l − 1 ] [0,l-1] [ 0 , l − 1 ] 之间满足条件的数。
所以我们就是询问 [ 0 , x ] [0,x] [ 0 , x ] 这段区间内有多少个数了。
把 x x x 的每一位 x i x_i x i 都写下来,最高位为 k k k 。
我现在要找 y y y 满足 0 ≤ y ≤ x 0\le{y}\le{x} 0 ≤ y ≤ x ,把 y y y 的每一位 y i y_i y i 也写下来,
也就是 y i y_i y i 有多少种方案使得每一位的 y ≤ x y\le x y ≤ x ,所以就从高位往低位依次去填,
状态 f i , 0 / 1 f_{i,0/1} f i , 0 / 1 代表从高到低 填到第 i i i 位了当前已经填的这些位是等于 x x x 的这些位还是小于 x x x 的这些位,这种情况下的方案数。
其中,0 → < , 1 → = 0\to<,1\to= 0 → < , 1 → =
初始化 f k + 1 , 1 = 1 f_{k+1,1}=1 f k + 1 , 1 = 1 ,第k + 1 k+1 k + 1 位两个数都是零,所以两数相等。
转移:
f i − 1 , 0 + = f i , 0 × 10 f_{i-1,0}+=f_{i,0}\times{10} f i − 1 , 0 + = f i , 0 × 1 0
因为高位已经小于了,所以再往低一位填 0 ∼ 9 0\sim{9} 0 ∼ 9 都可以,所以就是乘十。
1 2 for (int j=0 ;j<=x[i-1 ];j++) f[i-1 ][x[i-1 ]==j]+=f[i][1 ];
最后的答案就是 f 1 , 0 + f 1 , 1 f_{1,0}+f_{1,1} f 1 , 0 + f 1 , 1 代表 0 ∼ x 0\sim{x} 0 ∼ x 小于等于 x x x 的数的个数。
数位DP很常用一个前缀和优化。
状态设计常用 f i , j f_{i,j} f i , j ,i i i 表示填到第 i i i 位了,j j j 通常表示条件是否满足,以此决定下一位能填的数是多少,初始化就要保证两个数都没开始填的方案为 1 1 1 ,转移就枚举下一位填什么数。
数位之和求一个 [ l , r ] [l,r] [ l , r ] 每个数每一位加起来的结果。
先转为前缀和:[ 0 , x ] [0,x] [ 0 , x ]
还是老状态 g i , 0 / 1 g_{i,0/1} g i , 0 / 1 值便为此时的数位之和,在你填下一位的时候,要考虑当前有多少个数,而数位之和就会多出这么多倍,所以我还要维护这样的数有多少个,把上一个题的 f i , j f_{i,j} f i , j 代表这样的数有多少个。
转移和上面是同样的,只不过要加上当前填的数能填多少个 + = x × f i , j +=x\times{f_{i,j}} + = x × f i , j ,x x x 为当前位置填的数。
1 g[i-1 ][j&&(r==y[i-1 ])]+=g[i][j]+f[i][j]*r;
g g g 代表数位之和。
合并一下:
1 2 3 4 5 6 7 8 for (int i=k+1 ;i>=2 ;i--) for (int j=0 ;j<=1 ;j++) for (int r=0 ;r<=9 ;r++) { if (j==1 &&r>y[i-1 ]) continue ; f[i-1 ][j&&(r==y[i-1 ])]+=f[i][j]; g[i-1 ][j&&(r==y[i-1 ])]+=g[i][j]+f[i][j]*r; }
答案就是 g 1 , 0 + g 1 , 1 g_{1,0}+g_{1,1} g 1 , 0 + g 1 , 1 。
差二求 [ l , r ] [l,r] [ l , r ] 之间满足相邻两个数之间差至少为 2 2 2 的数有多少个
题目加了一个条件,很自然的想法就是再加一个维度,并且这个维度必然会影响我的转移,不然有啥价值。
设 f i , j , k f_{i,j,k} f i , j , k 前两维都一样,第三维记录了 i i i 这一位填什么。
填第 i − 1 i-1 i − 1 位的时候,使得 r − k ≥ 2 r-k\ge{2} r − k ≥ 2 就可以了。
P2657
注:而这个题还需要处理前导零的情况。
回文数求 [ l , r ] [l,r] [ l , r ] 之间有多少个回文数,不允许前导零存在。
设 f i , j , k f_{i,j,k} f i , j , k 前两维依旧相同,而找回文数的时候,我们不仅要比较高位与 x x x 的大小关系,还要比较低位与 x x x 的大小关系。
所以 k k k 取 0 , 1 , 2 0,1,2 0 , 1 , 2 ,分别代表小于,等于,大于。
j j j 代表正着填时与 x x x 的大小关系,而 k k k 代表对称那边与 x x x 的大小关系。
因为只要前面比 x x x 小,后面是可以比 x x x 大的,所以可以填 2 2 2 。
然后枚举一遍就可以了。
积K求 [ l , r ] [l,r] [ l , r ] 之间满足各位数字之积为 K K K 的数的个数,l , r , k ≤ 1 0 18 l,r,k\le{10^{18}} l , r , k ≤ 1 0 1 8 。
设 f i , j , r f_{i,j,r} f i , j , r 代表各位数之积为 r r r 的方案数,虽然开不下,但是先这么想再考虑优化。
假如使 K = 111 K=111 K = 1 1 1 ,那么一定在某个位置有 3 × 37 3\times{37} 3 × 3 7 ,而由于是一位一位的乘,所以最后它一定只有 4 4 4 个质因子:2 , 3 , 5 , 7 2,3,5,7 2 , 3 , 5 , 7 ,因为一旦有其它质因子就直接输出无解就可以了。
所以 f i , j , a , b , c , d f_{i,j,a,b,c,d} f i , j , a , b , c , d 把乘积为 r r r 换成了 乘积用 2 a + 3 b + 5 c + 7 d 2^a+3^b+5^c+7^d 2 a + 3 b + 5 c + 7 d 表示。
这个时候就可以正常进行 DP 了。
这样还没完,要考虑 K K K 是否为 0 0 0 ,如果为 0 0 0 ,那么一定存在某位是 0 0 0 ,需要再开一维记录前 i i i 位是否有 0 0 0 ;如果不是那么必然不存在某位为 0 0 0 。
还要处理一下前导零的情况。
F给定一个区间 [ l , r ] [l,r] [ l , r ] ,定义函数 f ( x ) f(x) f ( x ) 代表 x x x 每相邻两位差的绝对值之和,现在要在 [ l , r ] [l,r] [ l , r ] 之间找这样一个最大的 f ( x ) f(x) f ( x ) 。
这道题还能拆成前缀和来做吗?
max \max max 不可以通过前缀和来做的,所以 l , r l,r l , r 拆不开了。
定义 f i , j , k f_{i,j,k} f i , j , k 代表从高向低填到第 i i i 位,j j j 代表当前填的数与 r r r 的这些位数相比是小于还是等于,k k k 代表当前填的这些数与 l l l 的这些位数是大于还是等于。
既然有两个限度,那就再开一维记录。
同时还要记录一下当前这位填的是什么: f i , j , k , p f_{i,j,k,p} f i , j , k , p
这样进行转移就可以了。
棋盘在 n × m n\times{m} n × m 的棋盘上放若干个炮使得其互相不攻击的方案数。n , m ≤ 100 n,m\le{100} n , m ≤ 1 0 0
如果两个炮之间可以互相攻击,那么这一行或者这一列一定至少存在三个炮,所以每一行每一列放炮的数量就要 ≤ 2 \le{2} ≤ 2 。
用 f i , j , k , l f_{i,j,k,l} f i , j , k , l 代表前 i i i 行已经放好炮了,有 j j j 列有一个炮,有 k k k 列有一个炮,有 l l l 列有两个炮。
但是 j + k + l = m j+k+l=m j + k + l = m ,所以可以去除冗余变量,那就把 l l l 删掉,
那么最后就是 f i , j , k f_{i,j,k} f i , j , k 代表这种情况下的方案数。
转移:
已经放好了前 i i i 行的炮,现在要放第 i + 1 i+1 i + 1 行的炮,
放 0 0 0 个炮:f i + 1 , j , k + = f i , j , k f_{i+1,j,k}+=f_{i,j,k} f i + 1 , j , k + = f i , j , k 放 1 1 1 个炮:放在有 0 0 0 个炮的列:f i + 1 , j − 1 , k + 1 + = f i , j , k × j f_{i+1,j-1,k+1}+=f_{i,j,k}\times{j} f i + 1 , j − 1 , k + 1 + = f i , j , k × j 放在有 1 1 1 个炮的列:f i + 1 , j , k − 1 + = f i , j , k × k f_{i+1,j,k-1}+=f_{i,j,k}\times{k} f i + 1 , j , k − 1 + = f i , j , k × k 放 2 2 2 个炮 :一个放在 0 0 0 个炮的列,一个放在 1 1 1 个炮的列:f i , j − 1 , k + = f i , j , k × j × k f_{i,j-1,k}+=f_{i,j,k}\times{j}\times{k} f i , j − 1 , k + = f i , j , k × j × k 两个都放在 0 0 0 个炮的列:f i + 1 , j − 2 , k + 2 + = f i , j , k × C ( j , 2 ) f_{i+1,j-2,k+2}+=f_{i,j,k}\times{C(j,2)} f i + 1 , j − 2 , k + 2 + = f i , j , k × C ( j , 2 ) 两个都放在 1 1 1 个炮的列:f i + 1 , j , k − 2 + = f i , j , k × C ( k , 2 ) f_{i+1,j,k-2}+=f_{i,j,k}\times{C(k,2)} f i + 1 , j , k − 2 + = f i , j , k × C ( k , 2 ) 其实可以发现一个性质,在求方案数时,其实我们是不关心到底是怎么放的,只关心这样放会有多少种方案,所以要记录与方案有关的信息。
排列DP这种题会告诉你有一个 1 ∼ n 1\sim{n} 1 ∼ n 的排列,求其中满足某个条件的排列有多少个。 一共有 n ! n! n ! 个排列。
逆序对求 1 ∼ n 1\sim{n} 1 ∼ n 的排列种,逆序对数量位偶数的排列数。
答案是 n ! 2 \frac{n!}{2} 2 n ! ,但是怎么用DP来解?
假设这里有一个 1 ∼ i 1\sim{i} 1 ∼ i 的排列,在这里插入一个 i + 1 i+1 i + 1 ,就会变成一个 1 ∼ i + 1 1\sim{i+1} 1 ∼ i + 1 的排列,所以我就可以不断插入数来得到最终的排列。
设 f i , j f_{i,j} f i , j 已经处理完了所有 1 ∼ i 1\sim{i} 1 ∼ i 的排列(填到了 i i i )此时逆序对数量为 j j j 的方案数。
这就是排列DP常用的状态:1 ∼ i 1\sim{i} 1 ∼ i 的排列已经处理完了且满足条件数量为 j j j 的方案数。
初始化: f 1 , 0 = 1 f_{1,0}=1 f 1 , 0 = 1
转移:枚举第 i + 1 i+1 i + 1 个数插到哪里。有 i i i 个数,i + 1 i+1 i + 1 个位置,分别编号 0 ∼ i 0\sim{i} 0 ∼ i ,那么插入到第 k k k 个位置,由于我插入的是第 i + 1 i+1 i + 1 个数,因此它比之前所有数都大,也就是说第 k k k 个位置之后的数都比它小,这就是新产生的逆序对的个数 i − k i-k i − k 。
优化:只关心奇偶性,并不关心到底是多少,所以第二维模二即可。
1 2 3 4 5 f[1 ][0 ]=1 ; for (int i=1 ;i<n;i++) for (int j=0 ;j<=1 ;j++) for (int k=0 ;k<=i;k++) f[i+1 ][(j+i-k)%2 ]+=f[i][j];
排列DP不一定是从小到大或者从大到小插入,所以要根据不同情况定不同的状态,每次考虑下一个数怎么插入。
激动对于一个排列,它的激动值维序列中严格大于前面所有数的元素的个数。 给定 n n n 个数,求它们每个数的所有排列中,激动值不超过 k k k 的个数。
这道题就要从大到小插入。
设 f i , j f_{i,j} f i , j 代表从大到小已经把 i ∼ n i\sim{n} i ∼ n 的数插入进来了,这个时候的激动值是 j j j 的方案数。
这样每次插入的都是最小的数,无论把它插到哪里都不会影响原来的数的激动值,所以我们就只需要枚举它在哪里,只有它在最前面的时候激动值会加一,其它的时候激动值不变。
LIS1 ∼ n 1\sim{n} 1 ∼ n 的所有排列中,LIS 不超过 2 的排列数
设 f i , j f_{i,j} f i , j 代表已经插入了 1 ∼ i 1\sim{i} 1 ∼ i 最长上升子序列的个数
假设从小到大插,插入了 1 ∼ i 1\sim{i} 1 ∼ i ,下一个插入的是 i + 1 i+1 i + 1 ,前面所有的最长上升子序列要么是1要么是2。
最长上升子序列长度为1,当且仅当这个序列是降序排列,特殊处理一下即可。
最长上升子序列长度为2,i + 1 i+1 i + 1 一定不能插入到任何一个最长上升子序列的右边,所以我要插到所有长度为2的上升子序列中最左边的右端点的左边。
f i , j f_{i,j} f i , j 代表已经插完了 1 ∼ i 1\sim{i} 1 ∼ i ,此时最长上升子序列最左端的右端点是 j j j 。
这样就可以枚举 0 ∼ j − 1 0\sim{j-1} 0 ∼ j − 1 就可以了,把 i + 1 i+1 i + 1 插进去。
k ! = 0 : f i + 1 , j + 1 + = f i , j k!=0:f_{i+1,j+1}+=f_{i,j} k ! = 0 : f i + 1 , j + 1 + = f i , j k = 0 : f i + 1 , k + = f i , j k=0:f_{i+1,k}+=f_{i,j} k = 0 : f i + 1 , k + = f i , j 博弈DP DP 的单调队列优化对朴素 DP 进行等价变形。
对于一个对应右端点,要找到一个左端点使得左端点前缀和最小。
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 #include <iostream> #include <cstdio> #include <algorithm> #include <cstring> #include <climits> using namespace std;const int N = 300010 ;int n, m;int s[N], q[N];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) { scanf ("%d" , &s[i]); s[i] += s[i - 1 ]; } int res = INT_MIN; int hh = 0 , tt = 0 ; for (int i = 1 ; i <= n; i++) { if (q[hh] < i - m) hh++; res = max (res, s[i] - s[q[hh]]); while (hh <= tt && s[q[tt]] >= s[i]) tt--; q[++tt] = i; } printf ("%d\n" , res); return 0 ; }
破环成链。
所有前缀和都大于等于零,才能安全到达。
也就是区间和最小大于等于零,区间长度为 n n n 。
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 43 44 45 46 47 48 49 50 51 52 #include <iostream> using namespace std;const int N = 2e6 + 7 ;typedef long long LL;int n;LL s[N]; int o[N], d[N];int q[N];bool st[N];int main () { cin >> n; for (int i = 1 ; i <= n; i++) scanf ("%d%d" , &o[i], &d[i]); for (int i = 1 ; i <= n; i++) s[i] = s[i + n] = o[i] - d[i]; for (int i = 1 ; i <= n + n; i++) s[i] += s[i - 1 ]; int hh = 0 , tt = -1 ; for (int i = n + n; i; i--) { if (hh <= tt && q[hh] >= i + n) hh++; while (hh <= tt && s[q[tt]] >= s[i]) tt--; q[++tt] = i; if (i <= n) { if (s[q[hh]] >= s[i - 1 ]) st[i] = true ; } } d[0 ] = d[n]; for (int i = 1 ; i <= n; i++) s[i] = s[i + n] = o[i] - d[i - 1 ]; for (int i = 1 ; i <= n + n; i++) s[i] += s[i - 1 ]; hh = 0 , tt = -1 ; for (int i = 1 ; i <= n + n; i++) { if (hh <= tt && q[hh] < i - n) hh++; if (i > n) { if (s[q[hh]] <= s[i]) st[i - n] = true ; } while (hh <= tt && s[q[tt]] <= s[i]) tt--; q[++tt] = i; } for (int i = 1 ; i <= n; i++) if (st[i]) puts ("TAK" ); else puts ("NIE" ); return 0 ; }
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 #include <iostream> using namespace std;const int N = 200005 ;int n, m;int a[N];int f[N]; int q[N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) scanf ("%d" , &a[i]); int hh = 0 , tt = 0 ; for (int i = 1 ; i <= n; i++) { if (q[hh] < i - m) hh++; f[i] = f[q[hh]] + a[i]; while (hh <= tt && f[q[tt]] >= f[i]) tt--; q[++tt] = i; } int res = 1e9 ; for (int i = n - m + 1 ; i <= n; i++) res = min (res, f[i]); cout << res << endl; return 0 ; }
上一道题加一个二分答案即可。
注意边界问题。
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 #include <iostream> using namespace std;const int N = 5e4 + 7 ;int n, m;int w[N];int q[N];int f[N]; bool check (int mid) { int hh = 0 , tt = 0 ; for (int i = 1 ; i <= n; i++) { if (q[hh] < i - mid - 1 ) hh++; f[i] = f[q[hh]] + w[i]; while (hh <= tt && f[q[tt]] >= f[i]) tt--; q[++tt] = i; } for (int i = n - mid; i <= n; i++) if (f[i] <= m) return true ; return false ; } int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> w[i]; int l = 0 , r = n; while (l < r) { int mid = (l + r) >> 1 ; if (check (mid)) r = mid; else l = mid + 1 ; } cout << l << endl; return 0 ; }
f i f_i f i 表示从前 i i i 头奶牛中选且合法的所有方案中的最大价值。
不同点:第 i i i 头牛选或者不选。
不选的话就是 f i − 1 f_{i-1} f i − 1
选的话,最后一段的长度可能是 [ 1 , m − 1 ] [1,m-1] [ 1 , m − 1 ]
于是我们维护一段长度为 m m m 的滑动窗口,里面元素的值为 f j − 1 − s j f_{j-1}-s_j f j − 1 − s j 。
这样当 j = i − m j=i-m j = i − m 时刚好是 s i − s i − m s_i-s_{i-m} s i − s i − m 为 [ i − m + 1 , i ] [i-m+1,i] [ i − m + 1 , i ] ,这个时候 i − j i-j i − j 是不能选的,因此只能保留 f j − 1 f_{j-1} f j − 1
即 f i = f i − j − 1 + s i − s j f_i=f_{i-j-1}+s_i-s_j f i = f i − j − 1 + s i − s j
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 #include <iostream> using namespace std;const int N = 100010 ;typedef long long LL;int n, m;LL s[N]; LL f[N]; int q[N];LL g (int i) { return f[i - 1 ] - s[i]; } int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) { scanf ("%lld" , &s[i]); s[i] += s[i - 1 ]; } int hh = 0 , tt = 0 ; for (int i = 1 ; i <= n; i++) { if (q[hh] < i - m) hh++; f[i] = max (f[i - 1 ], g (q[hh]) + s[i]); while (hh <= tt && g (q[tt]) <= g (i)) tt--; q[++tt] = i; } cout << f[n] << endl; return 0 ; }
先看一维,每一个点存的是以它为右端点长度为 n n n 的区间的极值。
我们把所有行处理完了以后,再处理一遍列即可。
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <iostream> #include <algorithm> using namespace std;const int N = 1005 ;int n, m, k;int w[N][N];int row_max[N][N], row_min[N][N];int q[N];void get_min (int a[], int b[], int tot) { int hh = 0 , tt = -1 ; for (int i = 1 ; i <= tot; i++) { if (q[hh] <= i - k) hh++; while (hh <= tt && a[q[tt]] >= a[i]) tt--; q[++tt] = i; b[i] = a[q[hh]]; } } void get_max (int a[], int b[], int tot) { int hh = 0 , tt = -1 ; for (int i = 1 ; i <= tot; i++) { if (q[hh] <= i - k) hh++; while (hh <= tt && a[q[tt]] <= a[i]) tt--; q[++tt] = i; b[i] = a[q[hh]]; } } int main () { scanf ("%d%d%d" , &n, &m, &k); for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) scanf ("%d" , &w[i][j]); for (int i = 1 ; i <= n; i++) { get_min (w[i], row_min[i], m); get_max (w[i], row_max[i], m); } int res = 0x3f3f3f3f ; int a[N], b[N], c[N]; for (int i = k; i <= m; i++) { for (int j = 1 ; j <= n; j++) a[j] = row_min[j][i]; get_min (a, b, n); for (int j = 1 ; j <= n; j++) a[j] = row_max[j][i]; get_max (a, c, n); for (int j = k; j <= n; j++) res = min (res, c[j] - b[j]); } printf ("%d\n" , res); return 0 ; }