动态规划是为了求解一种包含大量重叠子问题的最优化问题的方法。基本思想是,将原问题分解为若干相似的子问题,在求解的过程中通过子问题的解求出原问题的解。听起来和分治法很相似,但是,分治法只是不断地将问题分解成小问题求解,而动规之所以优秀是它会进行类似于记忆化搜索的过程,在求解的过程中把每一个子问题的解保存下来(不管后面会不会用到),然后在求解更大的问题时,从这些子问题的解里面寻求当前问题的最优解。所以,从这里也可以看出动规解法的限定条件是,该问题有一个最优子结构,就是由它分解出的子问题的解也是最优的。
这样看起来是很抽象的,我们接下来一个个模型看下去吧。感觉对 dp 不太好分类,个人就做简单的整理,代码不对或是分类有问题请大家指出,谢谢!
首先是线性动规。
单调递增最长子序列
这个问题的基本模型是,给一个序列,要求它的最长递增子序列,子序列的值是递增的,它的坐标在原序列中也是递增的,但不要求连续。
如,给一串数字, 0 2 3 4 3 4 6 9 13 24 11 26,那么它的单调递增最长子序列就是 0 2 3 4 6 9 13 24 26
给一串字母,a f r e f j l m o d, 单调递增最长子序列就是 a e f j l m o
下面是求一个字母序列的单调递增子序列
原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=17
解1:
输入字符串为 str, 用一个数组 dp 记录问题的解, dp[i] 表示以 str[i] 结束的单调递增最长子序列的长度。
状态转移方程: dp[i] = max(1, dp[j] + 1) , str[j] < str[i]
复杂度为 O(n^2)
代码:
#include
#include
#include
using namespace std;
#define max(a,b) (a)>(b)?(a):(b)
int main()
{
int dp[10001],ans;
int T;
cin>>T;
while(T--)
{
ans = 1;
string str;
cin>>str;
int n = str.size();
for(int i = 0;i < n;++i)
{
dp[i] = 1;
for(int j = 0;j < i;++j)
{
if(str[i] > str[j])
{
dp[i] = max(dp[i],dp[j] + 1);
if(dp[i] > ans) ans = dp[i];
}
}
}
cout<
解二:
数组 dp 存不是答案子序列,但它和子序列长度是一样的,因为它的不断更新,所以总能确保答案正确,多看几遍。复杂度大概只有 O(100*N),因为最长单调递增子序列的长度是不可能超过127的。
#include
#include
#include
using namespace std;
//求最长单调递增子序列长度
int length(string str)
{
int dp[128], ans = 1;;
memset(dp,0,sizeof(dp));
int n = str.size();
dp[0] = str[0]; //初始化
for(int i = 0;i < n;++i)
{
for(int j = ans - 1;j >= 0; --j)
{
if(j == 0 && dp[0] > str[i]) dp[0] = str[i];
if(dp[j] < str[i])
{
dp[j+1] = str[i];
if(j == ans - 1) ans++;
break;
}
}
}
return ans;
}
int main()
{
int T;
string str;
cin>>T;
while(T--)
{
cin>>str;
cout<
单调递增最长子序列应用:
拦截导弹:
原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=79
题意:就是给一个序列然后求它的单调递减最长子序列的长度。
思路:按照最长单调递增子序列的思路,只要把递增的比较改为递减的比较就好,length1 函数为上面解法一的修改解法, length2 为上面解法二的修改解法,而且运用二分查找存放位置。
代码:
/*
* http://acm.nyist.net/JudgeOnline/problem.php?pid=79
*
* 求最长单调递减子序列长度
*/
#include
#include
#include
using namespace std;
#define MAXN 20 + 1
#define max(a,b) (a)>(b)?(a):(b)
int num[MAXN];
int dp[MAXN];
//直接 dp ,O(n^2)
int length1(int *num,int n,int *dp)
{
int ans = 1;
memset(dp,0,sizeof(dp));
for(int i = 0;i < n;++i)
{
dp[i] = 1;
for(int j = 0;j < i;++j)
{
if(num[j] > num[i])
{
dp[i] = max(dp[i],dp[j] + 1);
ans = max(ans,dp[i]);
}
}
}
return ans;
}
//求最长单调递减子序列长度,运用二分
int length2(int *num, int n, int *dp)
{
int ans = 1;;
memset(dp,0,sizeof(dp));
dp[0] = num[0]; //初始化
for(int i = 0;i < n;++i)
{
//二分查找 num[i] 应放的位置
int st = 0,ed = ans - 1;
while(st <= ed)
{
int mid = (st + ed)/2;
if(dp[mid] == num[i]) break;
if(dp[mid] > num[i])
{
st = mid + 1;
}
else
{
ed = mid - 1;
}
}
// st > ed 的时候才要将 num[i] 放进去,用纸演算一下,要放在 dp[st] 的位置
if(st > ed)
{
dp[st] = num[i];
if(st == ans) ans++;
}
}
return ans;
}
int main()
{
int N,M;
scanf("%d",&N);
while(N--)
{
scanf("%d",&M);
for(int i = 0;i < M;++i)
scanf("%d",&num[i]);
cout<
原题: http://www.rqnoj.cn/problem/26
题意:给 n 个整数,求一个子序列,有一个中间数 i, 使得 i 的左边递增,右边递减,要这个子序列最长。输出 n - length, length 为该子序列的长度。
思路:这还是单调递增最长子序列的问题,只是多了一步,要一个左边递增右边递减加起来最长的序列,数据较小,所以我们可以枚举。对队列里的每个数 i ,计算以它为中心的最长递增和最长递减,长度加起来和答案比较,更新 ans. 复杂度为 O(n^3) 。
代码:
/*
* http://www.rqnoj.cn/problem/26
*/
#include
#include
#include
#include
using namespace std;
#define maxn 103
int n,p[maxn],dp[maxn];
int ans;
int solve()
{
for(int i = 1;i <= n;++i)
{
for(int j = 1;j <= n;++j)
{
dp[j] = 1;
for(int k = 1;k < j;++k)
{
if(p[k] < p[j]) dp[j] = max(dp[j],dp[k]+1);
}
}
int l = dp[i];
for(int j = n;j >= i;--j)
{
dp[j] = 1;
for(int k = n;k > j;--k)
{
if(p[k] < p[j]) dp[j] = max(dp[j],dp[k]+1);
}
}
int r = dp[i];
ans = max(ans,l+r-1);
}
return n-ans;
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
for(int i = 1;i <= n;++i)
scanf("%d",&p[i]);
printf("%d\n",solve());
}
return 0;
}
这是线性 dp 的另一种类型。给出 n 个整数,求一个连续的子串,使得这个子串的和在所有子串中最大。
原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=44
思路:这道题的空间复杂度可以降到 O(1),不需要把所有的数存起来,只要一边输入一边判断就好。我们用 last 表示到前一位的最大子串和,初始化是 0, 那么以某个数结尾的最大子串和的最坏情况就是它本身,如果 last >0,到当前数的最大子串和肯定是当前数加上 last,然后动态更新 ans 就好。
代码:
/*
* http://acm.nyist.net/JudgeOnline/problem.php?pid=44
*/
#include
#include
using namespace std;
#define INF -99999999
#define max(a,b) (a)>(b)?(a):(b)
int main()
{
int T,n,last,cur;
scanf("%d",&T);
while(T--)
{
int sum = INF;
last = 0;
scanf("%d",&n);
for(int i = 0;i < n;++i)
{
scanf("%d",&cur);
//这是关键一步,如果前面的子串和 >0,那么这一步的最大子串和为这一个数加上前一步的最大子串和
if(last > 0) cur += last;
sum = max(sum,cur);
last = cur;
}
printf("%d\n",sum);
}
return 0;
}
原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=252
题意:长度为 n 的所有 0 1 串中,不含 11 的有多少个。
思路:dp[i] 表示长度为 i 的 0 1串不含 11 的有多少个。初始条件是 dp[1] = 2 dp[3] = 3
算dp[i] 的时候,如果 i 位为 0 ,那么前 i-1 位任取,所以 dp[i] += dp[i-1]
如果 i 位为 1,那么 i-1 位只能为 0,前 i-2 位任取,所以 dp[i] += dp[i-2]
状态方程: dp[i] = dp[i-1] + dp[i-2]
代码:
/*
* http://acm.nyist.net/JudgeOnline/problem.php?pid=252
dp[i]表示第i为不含11的01串个数
第i位为0,则前 i-1 位可任取
第i位为1,则 i-1 位必为0,前 i-2 位任取
所以 dp[i] = dp[i-1] + dp[i-2];
*/
#include
#include
using namespace std;
#define MAXN 42
int main()
{
int dp[MAXN],n;
dp[2] = 3;
dp[3] = 5;
for(int i = 4;i <= 40;++i)
dp[i] = dp[i-1] + dp[i-2];
cin>>n;
while(n--)
{
int k;
cin>>k;
cout<
原题:http://soj.me/1001
题意:使用一种加密方法,把 A-Z 映射为 1 - 26,那么根据一个加密后的序列,可能有多种解密方法,比如给 124,你可以理解成 1、2、4,也可以看成 12、4,或者1 、24,所以要求求出共有多少中可能。
思路:用 dp[i] 表示到第 i 位有多少种解密方法,则可以看到 dp[i] 和 dp[i-1] dp[i-2] 的关系。如果第 i 个数不是 0 ,那么这个数可以作为单独一个字母进行解码,前面 i-1 位任取,所以 dp[i] += dp[i-1] 。如果第 i-1 位是 1 或者 i-1 位是2,i 位是小于 7 的数,那么它们可以翻译为一个字母解码,前 i-2 位任取,所以 dp[i] += dp[i-2]。
代码:
/*
*http://soj.me/1001
*/
#include
#include
#include
using namespace std;
int main()
{
string str;
int ans[10000];
int n;
while(cin>>str&&str!="0")
{
memset(ans,0,sizeof(ans));
ans[0] = 1;ans[1] = 1;
n = str.size();
for(int i=1;i