多阶段决策问题——DAG(算法竞赛入门经典笔记)

多阶段决策问题——DAG

本文为算法竞赛入门经典第九章第三节的笔记(刘汝佳. 算法竞赛入门经典.第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];
            }
        }
  1. 使用记忆化搜索的方法
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;
}
  1. 打印输出字典序最小的策略
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;
}

0-1背包问题

物品无限的背包问题,和硬币问题一样,只需把无权图改成有权图即可。

物品有限的背包问题

只是凭“剩余体积”无法得知每个物品是否已经用过。
引入“阶段”,算法设计为:用 d(i,j) 表示当前在第 i 层,背包剩余容量为 j 时接下来的最大重量和。即为从 (i,j) 节点出发的最长路径。则

d(i,j)=max{d(i+1,j),d(i+1,jV[i])+W[i]}

d(i,j)==d(i+1,j) 的时候就是说 j 小于 i 以后的任何物品的容量,也就是说 j 容量装不下其他物品了。
即在这种情况下,最多的层数就是物品的种类数 n
通俗一点, d(i,j) 表示“把第 i i+1 i+2 , … , n 个物品装到容量为 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 必须逆序枚举。

使用记忆化搜索的方法
每种物品有装在背包里和不装在背包里两种选择。可以算作一个二叉树。
多阶段决策问题——DAG(算法竞赛入门经典笔记)_第1张图片
全部代码:

#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) 节点出发的最长路径。则

f(i,j)=max{f(i1,j),f(i1,jV[i])+W[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 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;
}

个人觉得递推法比记忆化搜索方法似乎至少多算了一倍的工作。

你可能感兴趣的:(DAG,算法,动态规划,多阶段决策,树)