动态规划,DP(Dynamic Programming),运筹学的一个分支,同时也是求解决策过程最优化问题的过程。它和贪心
一样,不是具体的某一种算法,而是一种思想。
DP的学习和使用,非常地具有挑战性,会使用DP,经常可以写出令人拍案叫绝的代码。所以它也是算法竞赛,数学建模等中的一个热点问题
DP优化是对方程进行等价变换
个人博客codeslogan: dynamic programming
f ( i , j ) f(i,j) f(i,j)存的是集合中所有选法的属性,是一个数
每个物品只能选一次,即放入/不放入背包,使利益最大化
01背包问题(状态转移方程讲解)深蓝
状态转移方程:
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w ) f[i,j] = max(f[i-1,j],f[i-1,j-v]+w) f[i,j]=max(f[i−1,j],f[i−1,j−v]+w)
二维
#include <iostream> #include <algorithm> using namespace std; const int N = 1010; int n, m; int v[N], w[N]; int f[N][N]; int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; for (int i = 1; i <= n; i++) { for (int j = 0; j <= m; j++) { // 当前背包容量小于物品重量 if (j < v[i]) f[i][j] = f[i-1][j]; // 表示不选 else f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]); // 表示选上 } } cout << f[n][m] << endl; return 0; }
一维
关于内层循环要为何要逆序遍历?
如果顺序遍历,那么数组中来自上一轮的状态,会因顺序原因被污染。导致出现需要上一轮的状态时,却已经被覆盖了的情况。
换言之,如果j
是顺序递增的话,则相当于f[i][j]
变得是由f[i][j-v[i]]
推出来的,
而不是由原来的f[i-1][j-v[i]]
推的。与我们的状态转移方程不符。
#include <iostream> #include <algorithm> using namespace std; const int N = 1010; int n, m; int v[N], w[N]; int f[N]; int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; 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; }
AcWing.0-1背包
物品无限量,同一个物品可以重复放入背包中
O(n^3),朴素做法
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1010; int n, m; // 物品数量,背包容量 int v[N], w[N]; // 体积,价值 int f[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 = 0; j <= m; j++) // 重量 for (int k = 0; k * v[i] <= j; k++) // 物品个数 f[i][j] = max(f[i][j], f[i-1][j-k*v[i]] + k*w[i]); cout << f[n][m] << endl; return 0; }
O(n^2)
对k进行优化,三维降二维,状态转移方程优化推导如下:
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , . . . ) f[i,j] = max(f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2v]+2w,...) f[i,j]=max(f[i−1,j],f[i−1,j−v]+w,f[i−1,j−2v]+2w,...)
因为i-1
个物品的最大值已经在上一轮循环确定了下来,所以我们只需讨论第i
个物品应该选多少个
将j
替换为j-v
f [ i , j − v ] = m a x ( f [ i − 1 , j − v ] , f [ i − 1 , j − 2 v ] + w , f [ i − 1 , j − 3 v ] + 2 w , . . . ) f[i,j-v] = max(f[i-1,j-v],f[i-1,j-2v]+w,f[i-1,j-3v]+2w,...) f[i,j−v]=max(f[i−1,j−v],f[i−1,j−2v]+w,f[i−1,j−3v]+2w,...)
f [ i , j − v ] + w = m a x ( f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , f [ i − 1 , j − 3 v ] + 3 w , . . . ) f[i,j-v]+w = max(f[i-1,j-v]+w,f[i-1,j-2v]+2w,f[i-1,j-3v]+3w,...) f[i,j−v]+w=max(f[i−1,j−v]+w,f[i−1,j−2v]+2w,f[i−1,j−3v]+3w,...)
将上述式子代入原方程,进行等价替换,得到状态转移方程:
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i , j − v ] + w ) f[i,j]=max(f[i-1,j],f[i,j-v]+w) f[i,j]=max(f[i−1,j],f[i,j−v]+w)
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1010; int n, m; int v[N], w[N]; int f[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 = 0; j <= m; j++) { if (j < v[i]) f[i][j] = f[i-1][j]; else f[i][j] = max(f[i-1][j], f[i][j-v[i]] + w[i]); } cout << f[n][m] << endl; return 0; }
降维
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1010; int n, m; int v[N], w[N]; int f[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 = v[i]; j <= m; j++) // 从小到大循环 f[j] = max(f[j], f[j-v[i]] + w[i]); cout << f[m] << endl; return 0; }
同时处理输入和状态转移
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1010; int n, m; int f[N]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) { int v, w; cin >> v >> w; for (int j = v; j <= m; j++) f[j] = max(f[j], f[j-v] + w); } cout << f[m] << endl; return 0; }
每个物品的数量不同
朴素作法
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 110; int v[N], w[N], s[N]; int f[N][N]; int n, 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 ++ ) for (int j = 0; j <= m; j++) for (int k = 0; k <= s[i] && k * v[i] <= j; k++) f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i]*k); cout << f[n][m] << endl; return 0; }
二进制优化
将一个物品的个数,表示成一段从1开始的二进制数
例如,200:1、2、4、8、16、32、64、73
为什么不选128呢?因为如果加128,可以表示的数就达到了255,超出了200。到64时,表示的数范围为127,补上一个73,就能够表示从1-200之间的任何一个数
物品种数xlog2(某种物品的数量,向上取整)
。因为我们在把物品数量拆成二进制表示时,要考虑到要用多少个数,才能表示出物品数量,得出上述结果#include <iostream> #include <algorithm> using namespace std; // 当有1000种物品,每种物品的数量为2000,得25000 const int N = 25000; // 留意开辟空间 int f[N]; int v[N], w[N]; int n, m; int main() { cin >> n >> m; int cnt = 0; for (int i = 1; i <= n; i++) { int a, b, s; cin >> a >> b >> s; // 二进制优化核心代码 int k = 1; while (k <= s) { cnt++; v[cnt] = a * k; w[cnt] = b * k; s -= k; k *= 2; } if (s > 0) { cnt++; v[cnt] = a * s; w[cnt] = b * s; } } n = cnt; // 0-1背包 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; }
每一组中选取一样物品加入背包
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 110; int n, m; int f[N]; int v[N][N], w[N][N], s[N]; int main() { cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> s[i]; for (int j = 0; j < s[i]; j++) { cin >> v[i][j] >> w[i][j]; } } for (int i = 1; i <= n; i++) for (int j = m; j >= 0; j--) for (int k = 0; k < s[i]; k++) if (v[i][k] <= j) f[j] = max(f[j], f[j-v[i][k]] + w[i][k]); cout << f[m] << endl; return 0; }
DP里的Hello World!
#include <iostream> #include <algorithm> using namespace std; const int N = 510, INF = 1e9; int n; int a[N][N], f[N][N]; int main() { cin >> n; for (int i = 1; i <= n; i++) for (int j = 1; j <= i; j++) scanf("%d", &a[i][j]); for (int i = 0; i <= n; i++) for (int j = 0; j <= i + 1; j++) // 每行需要多初始化一个,考虑到两边 f[i][j] = -INF; f[1][1] = a[1][1]; for (int i = 2; i <= n; i++) // 若从1开始,结果为负无穷,由前状态转移而来 for (int j = 1; j <= i; j++) // 从2开始才有前状态 f[i][j] = max(f[i-1][j-1] + a[i][j], f[i-1][j] + a[i][j]); int res = -INF; for (int i = 1; i <= n; i++) res = max(res, f[n][i]); cout << res << endl; return 0; }
AcWing.数字三角形
到这里为止,学习动态规划的感想是:根据题目,找到其中的一个状态,分析它是由前一个状态怎么转换得来。
比方说到下图中的6这个位置上,那么要求以6结尾的最长递增子序列要通过遍历它的前六个数,判断与它们的大小关系,再加1,取最大值。而前6个数的最长又是怎么求出来的?就是根据前5个数,以此类推
#include <iostream> #include <algorithm> using namespace std; const int N = 1010; int a[N], f[N], pre[N]; int n; void PrintPath(int v) { if (pre[v] == 0) { printf("%d", a[v]); return; } PrintPath(pre[v]); printf(" %d", a[v]); } 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; // 初始化,只有1个数以第i个数结尾 for (int j = 1; j < i; j++) { if (a[j] < a[i]) { if (f[i] < f[j] + 1) { f[i] = f[j] + 1; pre[i] = j; // 记录前一个点的下标,用于还原路径 } } } } int res = -1e9, t; for (int i = 1; i <= n; i ++ ) { if (f[i] > res) { res = f[i]; t = i; // 记录最长路径的最后一个下标 } } cout << res << endl; PrintPath(t); return 0; }
AcWing.最长上升子序列
s1
的前i
个字符和s2
的前j
个字符的最长公共子序列当两个字符相等时,容易理解,由 f [ i − 1 ] [ j − 1 ] + 1 f[i-1][j-1] + 1 f[i−1][j−1]+1转移而来
而当两个字符不等时,两个字符中一定有一个可以抛弃 f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] f[i-1][j], f[i][j-1] f[i−1][j],f[i][j−1]另
外一个可能存在于最长序列,也有可能不存在,取最大值避免了复杂的讨论情况
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1010; int n, m; char s1[N], s2[N]; int f[N][N]; int main() { cin >> n >> m >> s1 + 1 >> s2 + 1; for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= m; j++) { f[i][j] = max(f[i-1][j], f[i][j-1]); if (s1[i] == s2[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1); } cout << f[n][m] << endl; return 0; }
AcWing.最长公共子序列
i
堆石子到第j
堆石子合并所需的代价通过枚举区间长度
,找出代价的最小值
状态转移: f [ l ] [ k ] + f [ k + 1 ] [ r ] + s [ r ] − s [ l − 1 ] f[l][k] + f[k+1][r] + s[r] - s[l-1] f[l][k]+f[k+1][r]+s[r]−s[l−1]
最后一次转移加上它的代价
#include <iostream> #include <algorithm> using namespace std; const int N = 310; int n; int f[N][N]; int s[N]; int main() { scanf("%d", &n); for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]); // 前缀和,确定任一区间的权重 for (int i = 1; i <= n; i ++ ) s[i] += s[i-1]; // len=1的时候无须合并,所以从2开始 for (int len = 2; len <= n; len++) // 枚举所有长度为len的情况 for (int i = 1; i + len - 1 <= n; i++) { int l = i, r = i + len -1; f[l][r] = 1e8; // 在区间范围内进行划分 for (int k = l; k <= r; k++) f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r] - s[l-1]); } cout << f[1][n] << endl; return 0; }
AcWing.石子合并
状态表示中属性为count的DP问题
一个正整数n可以表示 成若干个正整数之和,形如: n = n 1 + n 2 + . . . + n k n = n_1 + n_2 + ... + n_k n=n1+n2+...+nk,其中 n 1 ≥ n 2 ≥ . . . ≥ n k , k ≥ 1 n_1 \ge n_2 \ge ... \ge n_k, k \ge 1 n1≥n2≥...≥nk,k≥1。
我们将这样的一种表示 称为正整数n的一种划分。
现在给定一个正整数n,请你求出n共有多少种不同的划分方法。
输入格式
共一行,包含一个整数n。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能 很大,输出结果请对 1 0 9 + 7 10^9+7 109+7取模。
数据范围
1 ≤ n ≤ 1000 1 \le n \le 1000 1≤n≤1000
转化为完全背包问题
1-i
当中选,使体积恰好是j
的数量
选几个i,就要在之前的状态中提前预留出位置,因此减去
f [ i , j ] = f [ i − 1 , j ] + f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + . . . + f [ i − 1 , j − s i ] f[i,j] = f[i-1,j]+f[i-1,j-i]+f[i-1,j-2i]+...+f[i-1,j-si] f[i,j]=f[i−1,j]+f[i−1,j−i]+f[i−1,j−2i]+...+f[i−1,j−si]
将j
等价替换为j-i
f [ i , j − i ] = f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + f [ i − 1 , j − 3 i ] + . . . + f [ i − 1 , j − s i ] f[i,j-i] = f[i-1,j-i]+f[i-1,j-2i]+f[i-1,j-3i]+...+f[i-1,j-si] f[i,j−i]=f[i−1,j−i]+f[i−1,j−2i]+f[i−1,j−3i]+...+f[i−1,j−si]
代入原式,得
f [ i , j ] = f [ i − 1 , j ] + f [ i , j − i ] f[i,j]=f[i-1,j]+f[i,j-i] f[i,j]=f[i−1,j]+f[i,j−i]
#include <iostream> using namespace std; const int N = 1010, INF = 1e9 + 7; int n; int f[N]; int main() { cin >> n; f[0] = 1; // 一个数都不选,总和为0时的方案 for (int i = 1; i <= n; i ++ ) for (int j = i; j <= n; j++) f[j] = (f[j] + f[j-i]) % INF; cout << f[n] << endl; return 0; }
计数类DP
i
,并且表示成j
个数的和的个数
j个数
中最小值为1和最小值大于1
状态转移方程为
f [ i , j ] = f [ i − 1 , j − 1 ] + f [ i − j , j ] f[i,j]=f[i-1,j-1]+f[i-j,j] f[i,j]=f[i−1,j−1]+f[i−j,j]
最后的结果需要对式求和 f [ n ] [ i ] , i = 1 , 2 , . . . , n f[n][i],i=1, 2,...,n f[n][i],i=1,2,...,n
#include <iostream> using namespace std; const int N = 1010, mod = 1e9 + 7; int n; int f[N][N]; int main() { cin >> n; f[0][0] = 1; for (int i = 1; i <= n; i++) for (int j = 1; j <= i; j++) f[i][j] = (f[i-1][j-1] + f[i-j][j]) % mod; int res = 0; for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod; cout << res << endl; return 0; }
AcWing算法基础课,欢迎大家报名,强推噢!!!y总带你感受算法之美!