动态规划(dp)有主要有记忆化搜索和递推两种实现形式。
首先来看一个递归的问题——汉诺塔问题
题意:有三根柱子分别为A,B,C,现在在A柱子上套着n个盘子,每一个盘子都比在它上面的的盘子大,现在需要将n个盘子全部从A移到C,移动过程中要保证每个柱子上,底部的盘子总是比在它上面的盘子大,求移动多盘子的过程。
分析:
将n个盘子从A借助B移动到C,可分3步实现:
1.将上面的n-1个盘子从A借助C移动到B
2.将最底部的第n个盘子从A直接移到C
3.将n-1个盘子从B借助A移动到C
代码:
void hanoi(int n,char A,char B,char C)
{
if(n==1)
{
moveAC(A,C);
return;
}
hanoi(n-1,A,C,B);
move(A,C);
hanoi(n-1,B,A,C);
}
动态规划问题往往也可以用递归(搜索)来求解,但在递归实现的过程中往往会大量地进行重复的调用,增加了时间复杂度,这时我们有两种策略:
1)增加标记数组,当某个搜索的状态已经调用过,我们就直接返回之前调用时得到的结果;
2)写出递推式,由搜索的最底层开始,借助底层已经得到的结果推出上面层的结果,最终得到搜索顶层的答案。
最长上升自序列问题(LIS)
题意:给出n个整数a1,a2...an组成序列,求最长的上升子序列(若i< j,则ai<aj)长度。如序列1,4,3,5,7,3,8,可选出最长上升子序列1,3,5,7,8。
分析:
时间复杂度为O(n^2)的做法:
dp[i]表示以ai为末尾的最长上升子序列长度,则有 当a[j] < a[i], dp[i] = max{dp[i], dp[j] + 1}
时间复杂度O(nlogn)的做法:
dp[i]表示长度为i的上升子序列末尾元素的最小值,则对于每个元素ai,二分查找数组dp,找到第一个dp[]值不小于ai的位置,将其值变为ai
例题:http://acm.nyist.net/JudgeOnline/problem.php?pid=17
代码:
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h>
#define INF 0x7fffffff
#define MOD 1000000007
using namespace std;
typedef long long ll;
char a[10005], dp[10005];
int main()
{
int n;
scanf("%d ", &n);
while(n--)
{
scanf("%s", a);
int len = strlen(a);
for(int i = 0; i <= len; i++)
{
dp[i] = 'z' + 5;
}
for(int i = 0; i < len; i++)
{
*lower_bound(dp, dp + len, a[i]) = a[i];
}
printf("%d\n", lower_bound(dp, dp + len, 'z' + 5) - dp);
}
return 0;
}
最长公共子序列问题(LCS)
题意:给定两个字符串s1,s2...sn和t1,t1...tm,求最长公共子序列长度
如序列abcda和becd,最长公共子序列为bcd。
分析:
dp[i][j]表示 s1...si 和 t1...tj 的对应的最长公共子序列长度
则 当si=tj,dp[i][j] = max{dp[i - 1][j - 1] + 1, dp[i - 1][j], dp[i][j - 1]}
否则,dp[i][j] = max{dp[i - 1][j], dp[i][j - 1]}
矩阵连成顺序问题
题意:矩阵A为p0*p1的矩阵,矩阵B为p1*p2的矩阵,则两个矩阵相乘运算量为p0 * p1 * p2,多个矩阵相乘满足结合律,即A * B * C = A * (B * C),改变运算顺序使得结果相同但运算量不同,给出n个矩阵组成的序列,求出将它们依次乘起来的最小运算量 。
分析:
dp[i][j]表示第i个矩阵到第j个矩阵相乘的最小运算量,则 dp[i][j] = min{dp[i][k] + dp[k + 1][j] + p(i-1) * pk * pj},其中k为 i 和 j 之间的矩阵
观察得 j - i 较大的dp[i][j]要由 j - i 较小的dp[i][j] 转移得到结果,所以需按照 j - i 递增的顺序递推。
划分数
题意:有n个无区别的物品,将它们划分成不超过m组,求出划分方法数。
分析:
dp[i][j] 表示将j个物品划分为小于等于i分的方法数,则有
dp[i][j] = dp[i][j - 1] + dp[i - 1][j]
其中,dp[i][j - i]为第j个组不为空的方法数,可理解为先在i组中各放一个,再将j - i个放在i组中
背包问题
0-1背包
题意:有n个重量和价值分别为wi,vi(0 <= i < n)的物品,从这些物品中挑选出总重量不超过W的物品,使得到的总价值尽量大,求出总价值的最大值。
分析:
dp[i][j] 表示考虑了前i个物品,总重量不超过 j 时得到的总价值最大值,则
当 j < wi,dp[i][j] = dp[i - 1][j]
否则,dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - wi] + vi},这样理解,在容量最大为 j 时,如果一定要取第 i 个物品,那么前 i - 1个物品所占的最大容量为 j - wi
完全背包
题意:与0-1背包不同的是,给出的是n种重量和价值为wi,vi的物品,即相当于0-1背包中每个物品可以选无限多次
分析:
dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - k * wi] + vi},该式中还需从0开始枚举k直到 k * wi > j
0-1背包和完全背包都可以使用一维数组来实现,根据dp[i][j]列出表,我们可以观察到
0-1背包中,第i行第j列的更新只与第i - 1行的第j列之前的元素有关,所以,我们可以用一维数组不记录行数 i,按 j 递减的顺序更新,第j个元素的更新后不影响j前 j 个元素的更新
完全背包中,第i行第j列更新后的结果可用于第i行第j列后面的元素更新,所以我们用一维数组同样不记录行数,但按 j 递增的顺序更新
#include <stdio.h>
#include <string.h>
int a[105], c[105], dp[100005];
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
#endif
int n, m, ans;
while(scanf("%d%d", &n, &m) != EOF)
{
if(!n && !m) break;
for(int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for(int i = 1; i <= n; i++)
scanf("%d", &c[i]);
memset(dp, -1, sizeof(dp));
dp[0] = 0;
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
{
if(dp[j] >= 0) dp[j] = c[i]; //用前i-1种物品可以组成j,则要组成j剩余第i种物品c[i]个
else if(j < a[i] || dp[j - a[i]] <= 0) //用第i种物品不能组成j
dp[j] = -1;
else dp[j] = dp[j - a[i]] - 1; //用前i种物品组成j,最多剩余第i种物品个数
}
ans = 0;
for(int i = 1; i <= m; i++)
{
if(dp[i] >= 0) ans++;
}
printf("%d\n", ans);
}
return 0;
}
最后讲一下超大背包问题,其实这不算dp问题了,需要使用位运算和折半枚举的技巧
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> P;
int n;
ll w[50], v[50], m;
long long int INF = (1 << 30) - 1;
P ps[1 << 20];
void solve()
{
int n2 = n / 2;
ll res = 0;
for(int i = 0; i < 1 << n2; i++) //先枚举在左边的物品中取物品的情况,总情况数为2^n2
{//枚举1-2^n2,每个数字i对应一种取物品的情况
ll sw = 0, sv = 0;
for(int j = 0; j < n2; j++)
{ //用位运算判断i这种情况时,有没有去第j个物品
if(i >> j & 1)
{
sw += w[j]; sv += v[j];
}
}
ps[i] = make_pair(sw, sv); //加入每种取物品的情况对应总重和总价值
}
sort(ps, ps + (1 << n2));
int mm = 1;
for(int i = 1; i < 1 << n2; i++)
{ //去掉多余情况
if(ps[mm - 1].second < ps[i].second)
{
ps[mm++] = ps[i];
}
}
for(int i = 0; i < 1 << (n - n2); i++)
{ //枚举在右边的物品中取物品的情况
ll sw = 0, sv = 0;
for(int j = 0; j < n - n2; j++)
{
if(i >> j & 1)
{
sw += w[n2 + j]; sv += v[n2 + j];
}
}
if(sw <= m)
{//每得到在右边取物品的情况,用二分查找寻找在左边取物品的情况
ll tv = (lower_bound(ps, ps + mm, make_pair(m - sw, INF)) - 1) -> second; //使得两部分总重相加小于等于且总价值尽量大
res = max(res, sv + tv);
}
}
printf("%lld\n", res);
}
int main()
{
while(scanf("%d%lld", &n, &m) != EOF)
{
for(int i = 0; i < n; i++)
{
scanf("%lld", &w[i]);
v[i] = w[i];
}
solve();
}
return 0;
}