动态规划入门 一,动态规划的关键概念 1.重叠子问题 动态规划是将一个多阶段问题分成几个相互联系的单阶段问题。这几个相互联系的单阶段问题一定要是同类型的子问题,这样分成多阶段决策才有意义,否则每次面对的都是不同的问题,也就失去了分成单阶段的意义。在《算法导论》中,把这种同类型的子问题叫做”重叠子问题“。重叠不是完全相同,而是同类型,可以通过解决子问题,然后合并子问题的解而得到原问题的解。
动态规划中子问题的“重叠”,体现为可以用一个多项式来表示每个子问题。利用子问题的初始解和递推多项式,可以求出每个子问题的解,最终求出整个问题的解。
2.最优子结构 用动态规划求解最优化问题的必要条件是描述最优解的结构。如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。动态规划用于求解最优化问题,且问题的最优解中包括了子问题的最优解,所以具有“最优子结构”。
当一个问题具有最优子结构时,提示我们动态规划可能会适用。动态规划以自底向上的方式来利用最优子结构,首先找到子问题的最优解,解决子问题,然后找到问题的一个最优解。也就是说,求解小问题,然后推出大问题的解。
3.无后效性 无后效性又称为马尔可夫性(Markov property),是一个概率论中的概念。无后效性是指如果给定某一阶段的状态,则此阶段以后过程的发展仅与此阶段的状态有关,而不受此阶段以前各阶段状态的影响。通常浓缩成一句简短的“未来与过去无关”。问题的历史状态不影响未来的发展,只能通过当前的状态去影响未来,当前的状态是以往历史的一个总结。
在利用动态规划方法求解多阶段决策问题时,过程的状态必须具有无后效性。具体地说,如果k阶段的值已经确定,则在k+1阶段只关心k阶段的值,而不用关心k阶段的值是怎么求出来的,也不用关心k阶段以前的阶段的值。
4.状态描述和初始状态 在使用动态规划解决问题时,要能正确地描述最优解的结构,对最优解的结构进行准确描述称为“状态描述”。状态描述确定后,可以根据描述找到子问题的初始值(边界值),也即初始状态,问题的最终解将依据初始状态逐步推导得出。
状态描述对解决问题至关重要,且它并没有看起来那么简单,在解决千变万化的实际问题时,经常需要复杂的分析才能构造出最优解的结构。因为对最优化问题的状态描述需要满足最优子结构和无后效性,才能满足用动态规划解决问题的前提。
5.状态转移 动态规划将问题分解成多个阶段的子问题,并定义了每一个阶段的状态描述,在问题求解过程中,每个状态均可以由前序的状态推导得出,这种由前序状态推导出当前阶段状态的过程称为状态转移。
如果要利用前序阶段子问题的结果解决现阶段的子问题,必须要能够建立前后阶段状态的转移关系。状态转移的转移方式通常可以用问题规模的多项式来表示,描述状态转移的多项式称为状态转移方程,也可以称为递推公式。状态转移方程确定了由一个状态到另一个状态的演变过程,列出状态转移方程也是求解动态规划问题中的一个关键点和难点。
二,动态规划的一般求解步骤 动态规划问题的核心步骤有定义问题的状态、列出状态转移方程、状态初始化和代码求解。
1.定义问题的状态 定义问题的状态是要明确状态表示什么含义,定义的状态需要满足最优子结构和无后效性,如果问题的状态定义得不对,会导致无法用动态规划来求解。
具体到代码中,就是要定义 dp 数组中每个元素的含义,以及 dp 数组中下标的含义。代码中一般都会用一个数组来存储所有子问题状态的值,这个数组可以是一维数组 dp[i] 或二维数组 dp[i] [j]等。明确状态的含义是列出状态转移方程和问题求解的前提。
2.列出状态转移方程 状态转移方程是指从前序状态推导出当前状态的推导关系,通常可以用问题规模的多项式来表示,所以也常称为递推公式。列出正确的状态转移方程,是动态规划问题中非常重要的一个步骤,也是一个比较困难的步骤。
具体到代码中,就是找到 dp[i] 与 dp[i-1],dp[i-2], … 的关系,用 dp[i-1],dp[i-2], … 来表示 dp[i] 。例如,用 dp[i] 表示 i 的阶乘,则 dp[i] = i * dp[i-1],用 dp[i] 表示斐波那契数列,则 dp[i] = dp[i-1] + dp[i-2] 。递推公式在不同问题中是千变万化的,所以要分析清楚问题,从定义状态时就开始考虑递推公式的推导,假如无法得到递推公式,可以重新分析问题的状态定义得是否适合。
3.状态初始化 在定义完状态和找到递推公式后,需要求解问题的初始值,才能有递推解决问题的起点。动态规划的每一个状态都是从前序状态推导而来的,所以一定有初始值,也就是状态转移的起点。不过,起始状态并不是一成不变的,要根据定义的状态,找到有意义的初始值。
具体到代码中,求初始值就是求出起始状态的值。例如,阶乘的初始值是 dp[1] = 1,斐波那契数列的初始值是 dp[1] = 1, dp[2] = 1。在具体问题中,有些问题的初始值是从下标 1 开始,有些问题的初始值是从下标 0 开始,还有些问题的初始值是从 n-1 开始,等等。有些问题需要一个初始值,有些问题需要多个初始值。所以在求初始值时要根据状态定义和递推公式来调整,具体问题具体分析。
4.代码求解 前面的三个步骤主要是对问题进行剖析,找到解决问题的方法。
完成前面的步骤后,我们需要根据思路编写代码、运行代码得到结果。相对于前面三步而言,编写代码其实是最简单的一步 ,动态规划的代码通常像套公式一样,因为只要经过了前面的分析,写代码只是把结果计算出来。虽然说,不编写代码并执行,就得不到结果,但是在动态规划中,关键在于解题思路,很多人甚至没有把编写代码当成一个步骤。
三,刷题完事 线性DP:
上楼梯 数字三角形
最长上升子序列 接龙数列
背包问题:
01背包问题 拼题A打卡奖励
完全背包问题 整数拆分 硬币问题
区间DP:
石子合并(弱化版)
数位DP:
windy 数 相邻差值
树形DP:
没有上司的舞会 状态压缩DP 单调队列优化DP 斜率优化DP
……
四,模板题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> using namespace std;const int N = 100 ;int dp[N];int main () { int n; cin >> n; dp[1 ] = 1 ; dp[2 ] = 2 ; dp[3 ] = 4 ; for (int i = 4 ; i <= n; i++) dp[i] = dp[i-1 ] + dp[i-2 ] + dp[i-3 ]; cout << dp[n] << 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 #include <bits/stdc++.h> using namespace std;const int N = 1010 ;int dp[N], f[N];int main () { int n; cin >> n; for (int i = 1 ; i <= n; i++) cin >> f[i]; int res = 1 ; for (int i = 1 ; i <= n; i++) { dp[i] = 1 ; for (int j = i - 1 ; j >= 1 ; j--) if (f[i] > f[j]) dp[i] = max (dp[i], dp[j] + 1 ); res = max (res, dp[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 #include <iostream> #include <vector> using namespace std;const int N=100010 ;int f[N];int main () { int n; cin>>n; vector<int > v; for (int i = 1 ;i <= n; i++) cin >> f[i]; v.push_back (f[1 ]); for (int i = 1 ;i <= n; i++) { if (f[i] > v.back ()) v.push_back (f[i]); else *lower_bound (v.begin (),v.end (),f[i]) = f[i]; } cout<< v.size () << 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 #include <iostream> #include <cstring> using namespace std;const int N = 510 ;int dp[N][N];int main () { int n; cin >> n; memset (dp, -0x3f , sizeof dp); for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= i; j++) { cin >> dp[i][j]; if (i != 1 || j != 1 ) dp[i][j] += max (dp[i-1 ][j], dp[i-1 ][j-1 ]); } int res = -0x3f3f3f3f ; for (int i = 1 ; i <= n; i++) res = max (res, dp[n][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 #include <iostream> using namespace std; const int N = 1010 ; int dp[N][N]; int v[N], w[N]; int main () { int n, m; 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++) { dp[i][j] = dp[i-1 ][j]; if (j >= v[i]) dp[i][j] = max (dp[i-1 ][j], dp[i-1 ][j-v[i]] + w[i]); } cout << dp[n][m] << endl; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std; const int N = 1010 ; int dp[N]; int v[N], w[N]; int main () { int n, m; 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++) dp[j] = max (dp[j], dp[j-v[i]] + w[i]); cout << dp[m] << 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 #include <iostream> using namespace std; const int N = 1010 ; int dp[N][N]; int v[N], w[N]; int main () { int n, m; 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++) for (int k = 0 ; j/v[i] >= k; k++) dp[i][j] = max (dp[i][j], dp[i-1 ][j - v[i] * k] + w[i] * k); cout << dp[n][m] << 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 #include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1010 ;int n, m;int v[N], w[N], dp[N][N];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++) { dp[j] = dp[j]; if (j >= v[i]) dp[i][j] = max (dp[i-1 ][j], dp[i][j-v[i]] + w[i]); } } cout << dp[n][m] << 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 #include <iostream> #include <cstring> using namespace std;const int N = 510 ;int dp[N][N];int n, f[N];int main () { cin >> n; for (int i = 1 ; i <= n; i++) { cin >> f[i]; f[i] += f[i-1 ]; } memset (dp, 0x3f , sizeof dp); for (int i = 1 ; i <= n; i++) dp[i][i] = 0 ; for (int i = 2 ; i <= n; i++) { for (int j = 1 ; i+j-1 <= n; j++) { int l = j, r = i + j - 1 ; for (int k = j + 1 ; k <= r; k++) dp[l][r] = min (dp[l][k-1 ] + dp[k][r] + f[r] - f[l-1 ], dp[l][r]); } } cout << dp[1 ][n] << endl; return 0 ; }
五,练习题 接龙数列 拼题A打卡奖励
整数拆分 硬币问题
奇怪的汉诺塔
装箱问题
分割等和子集
股票买卖 IV
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 <iostream> using namespace std;const int N = 100010 ;int f[N], dp[N];int main () { int n; cin >> n; for (int i = 0 ; i < n; i++) { cin >> f[i]; int l = f[i]%10 ; while (f[i] >= 10 ) f[i] /= 10 ; f[i] = f[i] * 10 + l; } int res = 0 ; for (int i = 0 ; i < n; i++) { dp[i] = 1 ; for (int j = i-1 ; j >= 0 ; j--) { if (f[i]/10 == f[j]%10 ) dp[i] = max (dp[j] + 1 , dp[i]); } res = max (res, dp[i]); } cout << n - 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 #include <iostream> using namespace std;const int N = 100010 ;int f[N], dp[N][20 ];int main () { int n; cin >> n; for (int i = 1 ; i <= n; i++) { cin >> f[i]; int t = f[i]%10 ; while (f[i] >= 10 ) f[i] /= 10 ; f[i] = f[i] * 10 + t; } for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= 9 ; j++) dp[i][j] = dp[i-1 ][j]; dp[i][f[i]%10 ] = max (dp[i][f[i]%10 ], dp[i-1 ][f[i]/10 ] + 1 ); } int res = 1 ; for (int i = 0 ; i <= 9 ; i++) res = max (res, dp[n][i]); cout << n - 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 #include <iostream> using namespace std;const int N = 1010 ;int v[N], w[N], dp[N][N];int main () { int n, m; cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i]; for (int i = 1 ; i <= n; i++) cin >> w[i]; for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) { dp[i][j] = dp[i-1 ][j]; if (j >= v[i]) dp[i][j] = max (dp[i-1 ][j], dp[i-1 ][j-v[i]]+w[i]); } cout << dp[n][m] << 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 #include <iostream> #include <cstring> using namespace std;const int N = 1010 ;int v[N], w[N], dp[N][30 *N];int main () { int n, m; cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i]; int sum = 0 ; for (int i = 1 ; i <= n; i++) { cin >> w[i]; sum += w[i]; } memset (dp, 0x3f , sizeof dp); dp[0 ][0 ] = 0 ; for (int i = 1 ; i <= n; i++) for (int j = 0 ; j <= sum; j++) { dp[i][j] = dp[i-1 ][j]; if (j >= w[i]) dp[i][j] = min (dp[i-1 ][j], dp[i-1 ][j-w[i]] + v[i]); } int t = sum; while (dp[n][t] > m) t--; cout << t << 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 #include <iostream> #include <cstring> using namespace std;const int N = 1010 ; int v[N], w[N], dp[30 *N];int main () { int n, m; cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i]; int sum = 0 ; for (int i = 1 ; i <= n; i++) { cin >> w[i]; sum += w[i]; } memset (dp, 0x3f , sizeof dp); dp[0 ] = 0 ; for (int i = 1 ; i <= n; i++) for (int j = sum; j >= w[i]; j--) dp[j] = min (dp[j], dp[j-w[i]] + v[i]); int t = sum; while (dp[t] > m) t--; cout << t << endl; return 0 ; }