动态规划 动态规划(Dynamic Programming,DP)是一种用来解决一类最优化问题的算法思想。简单来说,动态规划就是将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个求解过的子问题的解记录下来,这样当下次碰到相同的子问题时,就可以直接使用之前记录的结果,而不是重复计算。
关键:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划(DP)。
核心:如何设计状态和状态转移方程
0-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 #include <iostream> using namespace std;const int N=10000 ;int dp[N][N],w[N],v[N];int main () { int n,m; cin>>n>>m; for (int i=1 ;i<=n;i++)cin>>w[i]>>v[i]; for (int i=1 ;i<=n;i++) { for (int j=1 ;j<=m;j++) { if (j>=w[i]) dp[i][j]=max (dp[i-1 ][j],dp[i-1 ][j-w[i]]+v[i]); else dp[i][j]=dp[i-1 ][j]; } } cout<<"max = " <<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> using namespace std;const int N=10000 ;int dp[N],w[N],v[N];int main () { int n,m; cin>>n>>m; for (int i=1 ;i<=n;i++)cin>>w[i]>>v[i]; for (int i=1 ;i<=n;i++) { for (int j=m;j>=w[i];j--) { dp[j]=max (dp[j],dp[j-w[i]]+v[i]); } } cout<<"max = " <<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 25 26 27 28 29 30 31 #include <iostream> #include <cmath> using namespace std;const int N = 10000 ;int dp[N][N];int w[N],v[N];int main () { int n,m; cin>>n>>m; for (int i = 1 ; i <= n ; i ++) { cin>>w[i]>>v[i]; } for (int i = 1 ; i<=n ; i++) for (int j = 1 ; j<=m ; j++) { if (j>=w[i]) dp[i][j] = max (dp[i-1 ][j],dp[i][j-w[i]]+v[i]); else dp[i][j]=dp[i-1 ][j]; } 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 #include <iostream> #include <cmath> using namespace std;const int N = 1010 ;int dp[N];int w[N],v[N];int main () { int n, m; cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> w[i] >> v[i]; for (int i = 1 ; i <= n; i++) for (int j = w[i]; j <= m; j++) dp[j] = max (dp[j], dp[j - w[i]] + v[i]); cout << "maxn=" <<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 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <algorithm> using namespace std;#define maxn=200010 int arr[maxn],dp[maxn];int main () { int n; cin>>n; for (int i=0 ;i<n;i++) { cin>>arr[i]; } dp[0 ]=arr[0 ]; for (int i=1 ;i<n;i++) { dp[i]=max (arr[i],dp[i-1 ]+arr[i]); } int k=0 ; for (int i=1 ;i<n;i++) { if (dp[i]>dp[k]) { k=i; } } cout<<dp[k]<<endl; return 0 ; }
最长不下降子序列(LIS) 在一个数字序列中找到一个最长的子序列(可以不连续),使得这个序列是不下降的(非递减的).
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 <iostream> #include <algorithm> using namespace std;const int N=1000 ;int A[N],dp[N];int main () { int n; cin>>n; for (int i=1 ; i<=n; i++)cin>>A[i]; int ans=-1 ; for (int i=1 ; i<=n; i++) { dp[i]=1 ; for (int j=1 ; j<i; j++) { if (A[i]>=A[j]&&dp[j]+1 >dp[i]) { dp[i]=dp[j]+1 ; } } ans=max (dp[i],ans); } cout<<ans<<endl; return 0 ; }
最长公共子序列(LCS) 给定两个字符串(或者数字序列)A和B,求一个字符串,使得这个字符串是A和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 <iostream> #include <cstring> #include <algorithm> using namespace std;const int N=1010 ;char A[N],B[N];int dp[N][N];int main () { int n; cin>>n; gets (A+1 ); gets (B+1 ); int lenA=strlen (A+1 ); int lenB=strlen (B+1 ); for (int i=0 ;i<=lenA;i++) { dp[i][0 ]=0 ; } for (int j=0 ;j<=lenB;j++) { dp[0 ][j]=0 ; } for (int i=1 ;i<=lenA;i++) { for (int j=1 ;j<=lenB;j++) { if (A[i]==B[j]) { dp[i][j]=dp[i-1 ][j-1 ]+1 ; } else dp[i][j]=max (dp[i][j-1 ],dp[i-1 ][j]); } } cout<<dp[lenA][lenB]<<endl; return 0 ; }
最长回文子串 给出一个字符串S,求S的最长回文子串的长度
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 #include <cstdio> #include <cstring> #include <iostream> #include <string> using namespace std;const int maxn=10010 ;char S[maxn];int dp[maxn][maxn];int main () { gets (S); int len=strlen (S),ans=-1 ; memset (dp,0 ,sizeof (dp)); for (int i=0 ;i<len;i++) { dp[i][i]=1 ; if (i<len-1 ) { if (S[i]==S[i+1 ]) { dp[i][i+1 ]=1 ; ans=2 ; } } } for (int L=3 ;L<=len;L++) { for (int i=0 ;i+L-1 <len;i++) { int j=i+L-1 ; if (S[i]==S[j]&&dp[i+1 ][j-1 ]==1 ) { dp[i][j]=1 ; ans=L; } } } printf ("%d\n" ,ans); return 0 ; }
DAG(有向无环图)最长路 求整个DAG 的最长路径(即不固定起点和终点) 给定一个有向无环图 ,怎么求解整个图所有路径中权值之和最大的那条。
令dp[i]表示从i号顶点出发能获得的最长路径长度,这样所有dp[i]中的最大值就是整个DAG的最长路径。
1 2 3 4 5 6 7 8 9 10 11 int DP (int i) { if (dp[i] > 0 ) return dp[i]; for (int j = 0 ; j < n; ++j) { if (G[i][j] != INF){ dp[i] = max (dp[i], DP (j)+G[i][j]); } } return dp[i]; }
从出度为0的顶点出发的最长路径长度为0,因此边界为这些顶点的dp值为0。 但具体实现,可以对整个数组dp初始化为0,这样dp函数当前访问的顶点i的出度为0时,就会返回dp[i]= 0(以此为dp的边界),而出度不为0的顶点则会递归求解,递归过程中遇到已经计算过的顶点,则会直接返回对应的dp值。
求解最长路径 可以开一个int型数组choice来记录最长路径上顶点的后继顶点。【如果存在多条最长路径,把choice数组改为vector数组】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int DP (int i) { if (dp[i] > 0 ) return dp[i]; for (int j = 0 ; j < n; ++j) { if (G[i][j] != INF){ int temp = DP (j) + G[i][j]; if (temp > dp[i]){ dp[i] = temp; choice[i] = j; } } } return dp[i]; } void printPath (int i) { printf ("%d\n" , i); while (choice[i] != -1 ){ i = choice[i]; printf ("->%d\n" , i); } }
记录每次决策所选择的策略,然后在dp数组计算完毕之后,根据具体情况进行递归或迭代来获取方案。
实际例题 问题 A: 矩形嵌套
题目描述 有n个矩形,每个矩形可以用a,b来描述,表示长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中当且仅当a<c,b<d或者b<c,a<d(相当于旋转X90度)。例如(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)中。你的任务是选出尽可能多的矩形排成一行,使得除最后一个外,每一个矩形都可以嵌套在下一个矩形内。 输入 第一行是一个正正数N(0<N<10),表示测试数据组数, 每组测试数据的第一行是一个正正数n,表示该组测试数据中含有矩形的个数(n<=1000) 随后的n行,每行有两个数a,b(0<a,b<100),表示矩形的长和宽 输出 每组测试数据都输出一个数,表示最多符合条件的矩形数目,每组输出占一行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 样例输入 1 10 1 2 2 4 5 8 6 10 7 9 3 1 5 8 12 10 9 7 2 2 样例输出 5
代码如下:
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 #include <cstdio> #include <algorithm> #include <cstring> using namespace std;const int MAXN = 1010 ;const int INF = -1000000000 ;int dp[MAXN];int G[MAXN][MAXN];int edge[MAXN][MAXN];int n;int Dp (int i) { if (dp[i] > 0 ) return dp[i]; for (int j = 0 ; j < n; ++j) { if (G[i][j] != 0 ){ int temp = Dp (j) + G[i][j]; if (temp > dp[i]){ dp[i] = temp; } } } return dp[i]; } int main (int argc, char const *argv[]) { int N; scanf ("%d" , &N); while (N--){ memset (G, 0 , sizeof (G)); fill (dp, dp + MAXN, 0 ); scanf ("%d" , &n); int a = 0 , b = 0 , maxIndex; for (int i = 0 ; i < n; ++i) { scanf ("%d%d" , &edge[i][0 ], &edge[i][1 ]); if (edge[i][0 ] < edge[i][1 ]){ swap (edge[i][0 ], edge[i][1 ]); } if (edge[i][0 ] > a && edge[i][1 ] > b){ a = edge[i][0 ]; b = edge[i][1 ]; maxIndex = i; } for (int j = 0 ; j < i; ++j) { if (edge[i][0 ] > edge[j][0 ] && edge[i][1 ] > edge[j][1 ]){ G[i][j] = 1 ; }else if (edge[i][0 ] < edge[j][0 ] && edge[i][1 ] < edge[j][1 ]){ G[j][i] = 1 ; } } } int ans = Dp (maxIndex); printf ("%d\n" , ans + 1 ); } return 0 ; }