本文为算法竞赛入门经典第九章第三节的笔记(刘汝佳. 算法竞赛入门经典.第2版[M]. 清华大学出版社, 2014.)
多阶段决策问题:每作一次决策就可以得到解的一部分,当所有决策做完之后,所有的解就“浮出水面”了。
单向TSP问题:
给定一个n*m的数字表格,找到一条从左到右的路径,使得上面的数字和最小。输出字典序最小的行号路径(每次可以从(i,j),走到(i,j+1),(i+1,j),(i-1,j)循环无限延伸没有边界)
思路:使用DAG上动态规划问题的基本思路。
1. 读入数据
for(int i=0;ifor(int j=0;jcin >> tsp[i][j];
}
}
int dp(int i,int j){
int& ans=dtsp[i][j];
if(ytsp[i][j])
return ans;
if(j >= n-1){
ans=tsp[i][j];
ytsp[i][j]=1;
return ans;
}
int mmin,next3[3];
next3[0]=(i-1+m)%m;
next3[1]=i;
next3[2]=(i+1)%m;
sort(next3,next3+3);
ans=min(tsp[i][j]+dp(next3[0],j+1),tsp[i][j]+dp(next3[1],j+1))+1;
for(int ii=0;ii<3;ii++){
if(tsp[i][j]+dp(next3[ii],j+1) < ans){
ans=tsp[i][j]+dp(next3[ii],j+1);
retsp[i][j]=next3[ii];
}
}
ytsp[i][j]=1;
return ans;
}
int eend = min(dtsp[0][0],dtsp[1][0])+1;
int cur;
for(int i=0;i
if(dtsp[i][0] < eend){
eend=dtsp[i][0];
cur=i;
}
}
cout << cur+1 ;
for(int i=0;i
cur = retsp[cur][i];
cout << " " << cur+1;
}
完整程序:
#define LOCAL
#include
#include
#include
#include
//#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXN 92
using namespace std;
int tsp[11][101];
int m,n;
int dtsp[11][101];//记录从该点出发的深度
int ytsp[11][101];//记录该点是否被运算过
int retsp[11][101];//路径的记录
int dp(int i,int j){
int& ans=dtsp[i][j];
if(ytsp[i][j])
return ans;
if(j >= n-1){
ans=tsp[i][j];
ytsp[i][j]=1;
return ans;
}
int mmin,next3[3];
next3[0]=(i-1+m)%m;
next3[1]=i;
next3[2]=(i+1)%m;
sort(next3,next3+3);
ans=min(tsp[i][j]+dp(next3[0],j+1),tsp[i][j]+dp(next3[1],j+1))+1;
for(int ii=0;ii<3;ii++){
if(tsp[i][j]+dp(next3[ii],j+1) < ans){
ans=tsp[i][j]+dp(next3[ii],j+1);
retsp[i][j]=next3[ii];
}
}
ytsp[i][j]=1;
return ans;
}
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif // LOCAL
while(cin >> m >> n && m && n){
memset(ytsp,0,sizeof(ytsp));
for(int i=0;ifor(int j=0;jcin >> tsp[i][j];
}
}
for(int i=0;i0);
}
int eend = min(dtsp[0][0],dtsp[1][0])+1;
int cur;
for(int i=0;iif(dtsp[i][0] < eend){
eend=dtsp[i][0];
cur=i;
}
}
cout << cur+1 ;
for(int i=0;i1;i++){
cur = retsp[cur][i];
cout << " " << cur+1;
}
cout << endl;
cout<< eend << endl;
}
return 0;
}
物品无限的背包问题,和硬币问题一样,只需把无权图改成有权图即可。
只是凭“剩余体积”无法得知每个物品是否已经用过。
引入“阶段”,算法设计为:用 d(i,j) 表示当前在第 i 层,背包剩余容量为 j 时接下来的最大重量和。即为从 (i,j) 节点出发的最长路径。则
样例:
6
61
1000 5
800 2
400 5
300 5
400 3
200 2
(第一行表示物品种类,之后一行表示背包最大容量,之后第一列表示重量,第二列表示容量,最后)。
输出:
3100
完整程序(使用递推法):
#define LOCAL
#include
#include
#include
#include
//#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXN 92
using namespace std;
int V[102],W[102];
int C;
int d[102][10002];
int n;
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif // LOCAL
while(cin >> n&& n){
memset(d,0,sizeof(d));
cin >> C;
for(int i=1;i<=n;i++){
cin >> W[i] >> V[i];
}
for(int i=n;i>=1;i--){
for(int j=0;j<=C;j++){
d[i][j]=( i==n ? 0:d[i+1][j]);
if(j>=V[i]){
d[i][j] = max(d[i][j] , d[i+1][j-V[i]]+W[i]);
}
}
}
cout << d[1][C]<< endl;
}
return 0;
}
递推法的 i 必须逆序枚举。
使用记忆化搜索的方法
每种物品有装在背包里和不装在背包里两种选择。可以算作一个二叉树。
全部代码:
#define LOCAL
#include
#include
#include
#include
//#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXN 92
//int s[MAXN];
//int p[MAXN];
//char *s="`1234567890-=QWERTYUIIOP[]\\ASDFGHJKKL;'ZXCVBNM,./";
using namespace std;
int V[102],W[102];
int C;
int d[102][10002];
int n;
int dp(int i,int j){
int& ans=d[i][j];
if(ans > 0 || i>n){//如果已经计算过或者超出了范围
return ans;
}
ans=(i==n?0:d[i+1][j]);
if(j>=V[i]){
ans=max(ans,dp(i+1,j-V[i])+W[i]);
}
return ans;
}
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif // LOCAL
while(cin >> n&& n){
memset(d,0,sizeof(d));
cin >> C;
for(int i=1;i<=n;i++){
cin >> W[i] >> V[i];
}
cout << dp(1,C) << endl;
}
return 0;
}
边读入边计算:
#define LOCAL
#include
#include
#include
#include
//#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXN 92
//int s[MAXN];
//int p[MAXN];
//char *s="`1234567890-=QWERTYUIIOP[]\\ASDFGHJKKL;'ZXCVBNM,./";
using namespace std;
int V,W;
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif // LOCAL
while(cin >> n&& n){
memset(d,0,sizeof(d));
cin >> C;
for(int i=n;i>=1;i--){
cin >> W >> V;
for(int j=0;j<=C;j++){
d[i][j]=( i==n ? 0:d[i+1][j]);
if(j>=V){
d[i][j] = max(d[i][j] , d[i+1][j-V]+W);
}
}
}
cout << d[1][C]<< endl;
return 0;
}
也可以使用另外一种对称的状态定义:用 f(i,j) 表示“把前 i 个物品装到容量为 j 的背包中的最大总重量”。即为从 (i,j) 节点出发的最长路径。则
#define LOCAL
#include
#include
#include
#include
//#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXN 92
//int s[MAXN];
//int p[MAXN];
//char *s="`1234567890-=QWERTYUIIOP[]\\ASDFGHJKKL;'ZXCVBNM,./";
using namespace std;
int C;
int d[10002];
int n;
int V,W;
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif // LOCAL
while(cin >> n&& n){
memset(d,0,sizeof(d));
cin >> C;
for(int i=n;i>=1;i--){
cin >> W >> V;
for(int j=C;j>=C;j--){
if(j>=V){
d[j] = max(d[j] , d[j-V]+W);
}
}
}
cout << d[1][C]<< endl;
return 0;
}
个人觉得递推法比记忆化搜索方法似乎至少多算了一倍的工作。