背包
01背包
01背包问题通常为:给定 n n n 种物品和一个背包,第 i i i 个物品体积为 c i c_i c i ,价值为 w i w_i w i ,背包地容量为 C C C 。如何选择装入背包的物品,使装入背包中的物品的总价值最大?
以 HDU 2602 为例
题目
用自底向上的递推完成转移过程, d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] 表示把前 i i i 个物品(从第 1 1 1 个到第 i i i 个)装入容量为 j j j 的背包中获得的最大价值。假设现在递推到 d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] ,分两种情况:
(1) 第 i i i 个物品的体积比 j j j 还大,则不能装入当前背包,直接继承前 i − 1 i-1 i − 1 个物品装入容量为 j j j 的背包,即
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j]
d p [ i ] [ j ] = d p [ i − 1 ] [ j ]
(2) 第i个物品体积比j小,能装进背包,又分为两种:装或不装
取①②中的最大值,状态转移方程为
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − c [ i ] ] + w [ i ] , d p [ i − 1 ] [ j ] ) dp[i][j] = max(dp[i-1][j-c[i]]+w[i],dp[i-1][j])
d p [ i ] [ j ] = ma x ( d p [ i − 1 ] [ j − c [ i ]] + w [ i ] , d p [ i − 1 ] [ 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;const int N = 1001 ;int w[N],c[N],dp[N][N];int solve (int n,int C) { for (int i=1 ;i<=n;i++) for (int j=0 ;j<=C;j++){ if (c[i] > j) dp[i][j] = dp[i-1 ][j]; else dp[i][j] = max (dp[i-1 ][j],dp[i-1 ][j-c[i]]+w[i]); } return dp[n][C]; } int main (void ) { ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); int t; cin >> t; while (t--){ int n,C; cin >> n >> C; for (int i=1 ;i<=n;i++) cin >> w[i]; for (int i=1 ;i<=n;i++) cin >> c[i]; memset (dp,0 ,sizeof (dp)); cout << solve (n,C) << '\n' ; } return 0 ; }
滚动数组–dp空间优化
dp状态方程常常是二维以上的,占用空间多。从状态方程
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − c [ i ] ] + w [ i ] , d p [ i − 1 ] [ j ] ) dp[i][j] = max(dp[i-1][j-c[i]]+w[i],dp[i-1][j]) d p [ i ] [ j ] = ma x ( d p [ i − 1 ] [ j − c [ i ]] + w [ i ] , d p [ i − 1 ] [ j ]) 可以看出, d p [ i ] [ ] dp[i][] d p [ i ] [ ] 只与 d p [ i − 1 ] [ ] dp[i-1][] d p [ i − 1 ] [ ] 有关,所以就用新的一行覆盖已经无用的一行(滚动),只需要两行就够了。
1 2 3 for (int i=1 ; i<=n; i++} for (int j=C; j>=c[i]; j--) dp[j] = max ( dp[j], dp[j-c[i]] + w[i] );
要注意:j j j 应该反过来循环,即从后向前覆盖 ,否则同一个物品可能会被装两次
优化后DP的空间复杂度从 O ( N × C ) O(N×C) O ( N × C ) 降低到 O ( C ) O(C) O ( C )
话不多说,来一道题
题目
这道题是分组背包,需要三层循环
代码
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 <bits/stdc++.h> using namespace std;int dp[101 ],c[101 ][101 ];void read (int N,int M) { memset (c,0 ,sizeof (c)); for (int i=1 ;i<=N;i++) for (int j=1 ;j<=M;j++) cin >> c[i][j]; } int solve (int N,int M) { memset (dp,0 ,sizeof (dp)); for (int i=1 ;i<=N;i++) for (int j=M;j>=0 ;j--) for (int k=1 ;k<=M;k++) if (j>=k) dp[j]=max (dp[j],dp[j-k]+c[i][k]); return dp[M]; } int main (void ) { ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); int N,M; cin >> N >> M; while (N || M){ read (N,M); cout << solve (N,M) << '\n' ; cin >> N >> M; } return 0 ; }
完全背包
完全背包是01背包的一种变式,和01背包的区别就是,01背包是每个物品最多选择1次,但完全背包的每个物品可以选择无限次。
完全背包可以这么来描述:我们有N N N 种物品和一个容量为V V V 的背包。对于第i i i 种物品,它的花费是c i c_i c i ,价值是w i w_i w i ,并且每种物品都可选择无限次。题目通常让我们求 在不超过背包总容量的前提下,选择若干件物品放入背包,使得总价值最大。
完全背包的状态转移式开一个维度就可以,我们定义d p [ j ] dp[j] d p [ j ] 为容量为j j j 时能达到的最大价值。
注意内层循环必须从小到大 遍历容量 j
,这和01背包的滚动数组优化是相反的。
状态转移方程为:
d p [ j ] = max ( d p [ j ] , d p [ j − w i ] + v i ) \quad dp[j] = \max(dp[j], dp[j - w_i] + v_i)
d p [ j ] = max ( d p [ j ] , d p [ j − w i ] + v i )
内层循环必须从小到大的原因是在计算 d p [ j ] dp[j] d p [ j ] 时,我们需要的d p [ j − w i ] dp[j-w_i] d p [ j − w i ] 正是本轮(第i i i 个物品)处理过之后的值。正序遍历可以保证当我们计算 dp[j]
时,dp[j-w_i]
已经被本轮更新过,它储存的已经是考虑了第 i i i 个物品后的最优解。
以下是洛谷P1616 ,完全背包的模板。
P1616 疯狂的采药
题目描述
LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是 LiYuxiang,你能完成这个任务吗?
此题和原题的不同点:
1 1 1 . 每种草药可以无限制地疯狂采摘。
2 2 2 . 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
输入格式
输入第一行有两个整数,分别代表总共能够用来采药的时间 t t t 和代表山洞里的草药的数目 m m m 。
第 2 2 2 到第 ( m + 1 ) (m + 1) ( m + 1 ) 行,每行两个整数,第 ( i + 1 ) (i + 1) ( i + 1 ) 行的整数 a i , b i a_i, b_i a i , b i 分别表示采摘第 i i i 种草药的时间和该草药的价值。
输出格式
输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
数据规模与约定
对于 30 % 30\% 30% 的数据,保证 m ≤ 10 3 m \le 10^3 m ≤ 1 0 3 。
对于 100 % 100\% 100% 的数据,保证 1 ≤ m ≤ 10 4 1 \leq m \le 10^4 1 ≤ m ≤ 1 0 4 ,1 ≤ t ≤ 10 7 1 \leq t \leq 10^7 1 ≤ t ≤ 1 0 7 ,且 1 ≤ m × t ≤ 10 7 1 \leq m \times t \leq 10^7 1 ≤ m × t ≤ 1 0 7 ,1 ≤ a i , b i ≤ 10 4 1 \leq a_i, b_i \leq 10^4 1 ≤ a i , b i ≤ 1 0 4 。
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 #include <bits/stdc++.h> using namespace std;using ll = long long ;using ull = unsigned long long ;using pii = pair<int ,int >;using pll = pair<ll,ll>;using pull = pair<ull,ull>;using vec = vector<int >;using vecl = vector<ll>; using maii = map<int ,int >;#define pb(x) push_back(x) #define eb(x) emplace_back(x) int main (void ) { ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); int t, m; cin >> t >> m; vector <ll> dp (t+5 ), c (m+5 ), w (m+5 ); for (int i=1 ;i<=m;i++) cin >> c[i] >> w[i]; dp[0 ] = 0 ; for (int i=1 ;i<=m;i++){ for (ll j=c[i];j<=t;j++){ dp[j] = max (dp[j], dp[j-c[i]] + w[i]); } } cout << dp[t]; return 0 ; }
这里再对比、强调一下01背包和完全背包的内层循环的区别:
0-1背包 :for (int j = V; j >= w[i]; j--)
(逆序)
完全背包 :for (int j = w[i]; j <= V; j++)
(正序)
变式:恰好装满背包
有些题目要求背包必须被恰好装满 ,而不是“不超过容量”,比如牛客周赛102的E和F题。
这类题的做法:
将d p [ 0 ] dp[0] d p [ 0 ] 初始化为0,表示什么都不装,价值为0,是起点;其它的初始化为正无穷或负无穷大。
状态转移式前需要加一个判断if(dp[j - w[i]] != -/+ INF)
,表示从上一个状态转移到这个状态时,上一个状态一定要是一个可达的、合法的状态(也就是不为初始化的无穷值),才能进行状态转移。
1 2 3 4 5 6 7 memset (dp, -0x3f , sizeof (dp));dp[0 ] = 0 ; for (..;..;..) for (..;..;..) if (dp[j - w[i]] != -INF) dp[j] = max (dp[j], dp[j - w[i]] + v[i]);
变式:求方案总数
如果问题是要求装满背包的方案总数 ,而不是最大价值,那么就需要再变一下。
我们定义状态:d p [ j ] dp[j] d p [ j ] 表示装满容量为j j j 的背包的方案数。我们初始化d p [ 0 ] = 1 dp[0] = 1 d p [ 0 ] = 1 ,表示什么都不装只有这一种方案;其余的初始化为0.
d p [ j ] dp[j] d p [ j ] 的方案数,应该等于所有可以转移到它的状态的方案数之和。对于第i i i 个物品,我们可以用它来构成j j j 的容量,那么d p [ j ] dp[j] d p [ j ] 就应该加上从d p [ j − w i ] dp[j-w_i] d p [ j − w i ] 转移过来的方案数。
d p [ j ] = d p [ j ] + d p [ j − w i ] dp[j] = dp[j] + dp[j - w_i]
d p [ j ] = d p [ j ] + d p [ j − w i ]
这里和普通完全背包的区别就是把 max
操作改为了 +
。
状压DP
状压DP是DP的一种优化方法
Hamilton旅行商问题
Hamilton问题是NP问题,没有多项式复杂度的解法,暴力解法复杂度是 O ( n × n ! ) O(n \times n!) O ( n × n !) ,而状压DP可将复杂度将为 O ( n 2 × 2 n ) O(n^2 \times 2n) O ( n 2 × 2 n )
设 S S S 为图的一个子集;
d p [ S ] [ j ] dp[S][j] d p [ S ] [ j ] :表示“集合 S S S 内的最短Hamilton路径”,即从起点 0 0 0 出发经过 S S S 中所有点,到达终点 j j j 时的最短路径;集合 S S S 中包括 j j j 点;
让 S S S 从最小的子集逐步拓展到整个图,最后得到的 d p [ N ] [ n − 1 ] dp[N][n-1] d p [ N ] [ n − 1 ] 就是答案, N N N 表示包含图上所有点的集合。
( S − j S-j S − j 表示从集合 S S S 中去掉 j j j ,即不包含 j j j 点的集合)
设 k k k 为 S − j S-j S − j 中一个点,把从 0 0 0 到 j j j 的路径分为两部分:( 0 → ⋯ → k ) + ( k → j ) (0 \rightarrow \dots \rightarrow k)+(k \rightarrow j) ( 0 → ⋯ → k ) + ( k → j ) 。以 k k k 为变量枚举 S − j S-j S − j 中所有的点,找出最短路径
状态转移方程为:
d p [ S ] [ j ] = m i n ( d p [ S − j ] [ k ] + d i s t ( k , j ) ) dp[S][j] = min(dp[S-j][k] + dist(k,j))
d p [ S ] [ j ] = min ( d p [ S − j ] [ k ] + d i s t ( k , j ))
状态压缩DP的技巧:用一个二进制数表示集合 S S S ,即把 S S S 压缩到一个二进制数中, S S S 的每一位表示图上的一个点, 0 0 0 表示不包含, 1 1 1 表示包含
例如 S = 00000101 S = 0000 0101 S = 00000101 ,表示包含 2 , 0 2,0 2 , 0 这两个点
if((S>>j) & 1)
,判断当前的集合 S S S 中是否含有 j j j 点
if((S^(1<<j))>>k & 1)
,其中S^(1<<j)
的作用是从集合中去掉 j j j 点,得到集合 S − j S-j S − j ,然后>> k & 1
表示用 k k k 遍历集合中的 1 1 1 ,这些 1 1 1 就是 S − j S-j S − j 中的点,这样就实现了"枚举集合 S − j S-j S − j 中所有的点" (S^(1<<j))
也可以写成S-(1<<j)
Code
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,dp[1 <<20 ][21 ];int dist[21 ][21 ];int main (void ) { ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); memset (dp,0x3f ,sizeof (dp)); cin >> n; for (int i=0 ;i<n;i++) for (int j=0 ;j<n;j++) cin >> dist[i][j]; dp[1 ][0 ] = 0 ; for (int S=1 ;S<(1 <<n);S++) for (int j=0 ;j<n;j++) if ((S>>j) & 1 ) for (int k=0 ;k<n;k++) if ((S^(1 <<j)) >> k & 1 ) dp[S][j] = min (dp[S][j],dp[S^(1 <<j)][k] + dist[k][j]); cout << dp[(1 <<n)-1 ][n-1 ] << '\n' ; return 0 ; }
树形DP
首先是树的储存,树的储存是图的储存的特殊情况,可以用邻接表储存,或链式向前星
二叉苹果树
洛谷P2015
定义状态 d p [ u ] [ j ] dp[u][j] d p [ u ] [ j ] 表示以节点 u u u 为根的子树上留 j j j 条边时的最多苹果数量。 d p [ 1 ] [ q ] dp[1][q] d p [ 1 ] [ q ] 就是答案。
状态转移方程:
d p [ u ] [ j ] = m a x ( d p [ u ] [ j ] , d p [ u ] [ j − k − 1 ] + d p [ v ] [ k ] + w ) dp[u][j] = max(dp[u][j], dp[u][j-k-1] + dp[v][k] + w)
d p [ u ] [ j ] = ma x ( d p [ u ] [ j ] , d p [ u ] [ j − k − 1 ] + d p [ v ] [ k ] + w )
其中,v v v 是 u u u 的一个子节点。 d p [ u ] [ j ] dp[u][j] d p [ u ] [ j ] 的计算分以下两部分:
d p [ v ] [ k ] dp[v][k] d p [ v ] [ k ] :在 v v v 上留 k k k 条边
d p [ u ] [ j − k − 1 ] dp[u][j-k-1] d p [ u ] [ j − k − 1 ] :除了 v v v 上的 k k k 条边,以及 u − v u-v u − v 边,那么以u为根的这棵树上还有 j − k − 1 j-k-1 j − k − 1 条边,它们在 u u u 的其他子节点上
总复杂度小于 O ( n 3 ) O(n^3) O ( n 3 )
PS:
这道题是无向图储存,因为题目没有说明输入的两个节点哪个是爹哪个是儿,所以要push_back
两次,然后在搜的时候跳过father
j j j 循环必须递减,例如 d p [ u ] [ 5 ] dp[u][5] d p [ u ] [ 5 ] 会用到 d p [ u ] [ 4 ] dp[u][4] d p [ u ] [ 4 ] 和 d p [ u ] [ 3 ] dp[u][3] d p [ u ] [ 3 ] 等等,若是递减循环,先算 5 5 5 ,再算 4 4 4 ,再算 3 3 3 … \ldots … d p [ u ] [ 5 ] dp[u][5] d p [ u ] [ 5 ] 用到的是 d p [ u ] [ 4 ] dp[u][4] d p [ u ] [ 4 ] 和 d p [ u ] [ 3 ] dp[u][3] d p [ u ] [ 3 ] 的原值;若是递增循环, d p [ u ] [ 5 ] dp[u][5] d p [ u ] [ 5 ] 就会用到 d p [ u ] [ 4 ] dp[u][4] d p [ u ] [ 4 ] 和 d p [ u ] [ 3 ] dp[u][3] d p [ u ] [ 3 ] 的新计算后的值,就不对了。
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 #include <bits/stdc++.h> using namespace std;const int N = 200 ;int n,q;int dp[N][N], sum[N]; struct node { int v,w; node (int a,int b) : v (a),w (b) {} }; vector <node> e[N]; void dfs (int u,int father) { for (auto i:e[u]){ if (i.v == father) continue ; dfs (i.v,u); sum[u] += sum[i.v]+1 ; for (int j=min (q,sum[u]);j>=0 ;j--) for (int k=0 ;k<=min (sum[i.v],j-1 );k++) dp[u][j] = max (dp[u][j], dp[u][j-k-1 ] + dp[i.v][k] + i.w); } } int main (void ) { ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); cin >> n >> q; for (int i=1 ;i<n;i++){ int u,v,w; cin >> u >> v >> w; e[u].push_back ({v,w}); e[v].push_back ({u,w}); } dfs (1 ,0 ); cout << dp[1 ][q]; return 0 ; }
没有上司的舞会
典,上图
这题和上题有点区别
这题没有定义结构体,因为这题是有向无权图 ,题目已经表明了输入的节点中哪个是爹哪个是儿,而且没有边上没有权值。
定义状态 d p [ u ] [ 0 ] dp[u][0] d p [ u ] [ 0 ] 表示不选择当前节点的最优解, d p [ u ] [ 1 ] dp[u][1] d p [ u ] [ 1 ] 表示选择当前节点的最优解。
状态转移方程:
d p [ u ] [ 1 ] + = d p [ v ] [ 0 ] dp[u][1] += dp[v][0]
d p [ u ] [ 1 ] + = d p [ v ] [ 0 ]
表示选择该节点,就累加上不选子节点的最大值
d p [ u ] [ 0 ] + = m a x ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] ) dp[u][0] += max(dp[v][0], dp[v][1])
d p [ u ] [ 0 ] + = ma x ( d p [ v ] [ 0 ] , d p [ v ] [ 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 #include <bits/stdc++.h> using namespace std;const int N = 6666 ;int n, dp[N][2 ], w[N], father[N];vector <int > e[N]; void dfs (int u) { dp[u][0 ] = 0 ; dp[u][1 ] = w[u]; for (auto v:e[u]){ dfs (v); dp[u][1 ] += dp[v][0 ]; dp[u][0 ] += max (dp[v][0 ], dp[v][1 ]); } } int main (void ) { ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); cin >> n; for (int i=1 ;i<=n;i++) cin >> w[i]; for (int i=1 ;i<n;i++){ int u,v; cin >> v >> u; e[u].push_back (v); father[v] = u; } int t = 1 ; while (father[t]) t = father[t]; dfs (t); cout << max (dp[t][0 ], dp[t][1 ]) << '\n' ; return 0 ; }
区间DP
石子合并
以一道模板题目为例----石子合并
不能用贪心,会陷入局部最优解。用DP求解
定义 d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] 为合并第 i i i 堆到第 j j j 堆的最小花费
状态转移方程为:
d p [ i ] [ j ] = m i n ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + w [ i ] [ j ] ) , i ≤ k < j dp[i][j] = min(dp[i][k] + dp[k+1][j] + w[i][j]),\ i \leq k < j
d p [ i ] [ j ] = min ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + w [ i ] [ j ]) , i ≤ k < j
w [ i ] [ j ] w[i][j] w [ i ] [ j ] 表示第 i i i 堆到第 j j j 堆的石子总数,可以用前缀和计算。d p [ 1 ] [ n ] dp[1][n] d p [ 1 ] [ n ] 就是答案
复杂度 O ( n 3 ) O(n^3) O ( n 3 ) ,可以用四边形不等式优化到 O ( n 2 ) O(n^2) O ( n 2 )
自顶向下的思路:
计算大区间 [ i , j ] [i,j] [ i , j ] 的最优值时,合并它的两个子区间 [ i ] [ k ] [i][k] [ i ] [ k ] 和 [ k + 1 ] [ j ] [k+1][j] [ k + 1 ] [ j ] ,对所有可能的合并 ( i ≤ k < j ) (i \leq k < j) ( i ≤ k < j ) 采取最优合并。子区间再分解为更小的区间,最小区间 [ i , i + 1 ] [i,i+1] [ i , i + 1 ] 只包含两堆石子
自底向上的编程:
先在小区间进行DP得到最优解,再逐步合并小区间为大区间。以下是code:
1 2 3 4 5 6 7 8 9 10 11 12 const int INF = 0x3f3f3f3f ;int dp[n][n] {}; for (int len = 2 ; len <= n; len++){ for (int i = 1 ; i <= n - len + 1 ; i++){ int j = i + len - 1 ; dp[i][j] = INF; for (int k = i; k < j; k++){ dp[i][j] = min (dp[i][j], dp[i][k] + dp[k+1 ][j] + sum[j] - sum[i-1 ]); } } }
字符串区间
大致题意:两个长度相等的小写字母字符串 A A A 和 B B B 。定义一次操作:把 A A A 的一个连续子串(区间)都转换为某个字符串(就像用刷子刷成一样的字符)。要把 A A A 转换成 B B B ,所需最少操作数是多少? s t r l e n ≤ 100 strlen \leq 100 s t r l e n ≤ 100
input:
zzzzzfzzzzz
abcdefedcba
output:
6
两部分dp:1.从空白串转换到 B B B 。2.从 A A A 转换到 B B B
从空白串转换到 B B B :
① B [ i ] = = B [ j ] B[i] == B[j] B [ i ] == B [ j ] ,原区间 [ i , j ] [i,j] [ i , j ] 的最少操作次数 等于 分别去掉两个端点的区间的最少操作次数,例如 B = a b b b a B = abbba B = abbba ,最少刷两遍,第一遍刷 a a a a a aaaaa aaaaa ,第二遍刷 b b b bbb bbb ;去掉头变为 b b b a bbba bbba ,也要刷两遍;去掉尾变 a b b b abbb abbb ,也要刷两遍。
② B [ i ] ≠ B [ j ] B[i] \not= B[j] B [ i ] = B [ j ] ,用标准的区间操作,把区间分为 [ i , k ] [i,k] [ i , k ] 和 [ k + 1 , j ] [k+1,j] [ k + 1 , j ] 两部分来dp
从A转换到B
① A [ j ] = = B [ j ] A[j] == B[j] A [ j ] == B [ j ] ,不用转换,有 d p [ 1 ] [ j ] = d p [ 1 ] [ j − 1 ] dp[1][j] = dp[1][j-1] d p [ 1 ] [ j ] = d p [ 1 ] [ j − 1 ]
② A [ j ] ≠ B [ j ] A[j] \not= B[j] A [ j ] = B [ j ] ,用标准的区间操作,把区间分为 [ i , k ] [i,k] [ i , k ] 和 [ k + 1 , j ] [k+1,j] [ k + 1 , j ] 两部分来dp;这里利用了从空白串转化为 B B B 的结果,当区间 [ k + 1 , j ] [k+1,j] [ k + 1 , j ] 内的A和B字符不同时,从A转化到B与从空白串转化到B是一样的
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 inf = 0x3f3f3f3f ;int dp[102 ][102 ];string a,b; void solve (const int & n) { for (int len=2 ;len<=n;len++){ for (int i=1 ;i<=n-len+1 ;i++){ int j = i+len-1 ; dp[i][j] = inf; if (b[i] == b[j]) dp[i][j] = dp[i+1 ][j]; else for (int k = i;k < j;k++) dp[i][j] = min (dp[i][j], dp[i][k] + dp[k+1 ][j]); } } for (int j=1 ;j<=n;j++){ if (a[j] == b[j]) dp[1 ][j] = dp[1 ][j-1 ]; else for (int k=1 ;k<j;k++) dp[1 ][j] = min (dp[1 ][j], dp[1 ][k] + dp[k+1 ][j]); } cout << dp[1 ][n] << '\n' ; } int main (void ) { ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); while (cin >> a && cin >> b){ int n = a.size (); a = ' ' + a; b = ' ' + b; for (int i=1 ;i<=n;i++) dp[i][i] = 1 ; solve (n); } return 0 ; }