紫书第九章-----动态规划初步(DAG上的动态规划之硬币问题)

本文参考刘汝佳《算法竞赛入门经典》(第2版)*
动态规划的核心是状态和状态转移方程**

硬币问题

这里写图片描述
【分析】
类似于矩形嵌套问题,这个题目也属于DAG上的动态规划问题。d(i)表示凑得钱数之和为i所需要的最大或最小硬币数,若当前状态是i,用硬币j后,状态转移到i-Vj,每个状态看成一个点,这些点就形成了一个DAG图。硬币使用数目最大和最少分别对应了DAG图中的最长路和最短路。与嵌套矩形问题相比,若初始状态为S,末状态是0(若达不到0,题目无解,稍后解释),就类似于大矩形嵌套小矩形,大矩形到小矩形之间有边,即如果存在Vj,使得d(i)=d(i-Vj)+1,那么i与i-Vj之间有边。若初状态为0,末状态为S(若达不到S,题目无解,稍后解释),就类似于小矩形嵌套到大矩形,小矩形到大矩形之间有边,即如果存在Vj,使得d(i)=d(i-Vj)+1,那么i-Vj与i之间有边。我们以最长路为例。

  • 状态转移方程:d(i)=max{d(i-Vj)+1 | i>=Vj}
  • 即:d(i)=max{d(j)+1 | (i,j)∈E}(d(i)表示从i出发的最长路,类似于大矩形套小矩形,此时建图是从状态S到状态0建图)
  • 再考虑下,如果是以i结束的最长路,那么状态转移方程则是:d(i)=max{d(j)+1 | (j,i)∈E},这类似于小矩形嵌套在大矩形中,d(i)表示以i结束的最长路,此时建立的图是从状态0到状态S建图

无解分析:比如只有一种面值为3元的硬币,要凑够10元,这显然无解。

使用状态转移方程d(i)=max{d(j)+1 | (i,j)∈E}和记忆化搜素方法求最大最小硬币数

具体可以结合紫书,看下面的代码!笔者进行了详细研究并实现。注意要心中有图,本题建图的过程是从S状态出发向0状态推进的,本博文研究的两个状态转移方程的求解都是建立在从S状态到0状态推进建图的基础上的。

#include<iostream>
#include<cstring>
using namespace std;

const int maxn=105;

int coin[maxn];//记录各类硬币面值
int coin_cnt[maxn];//记录每种面值硬币使用数
int n;//硬币种类数
int S;//要凑到的目标总钱数
int d_max[maxn*maxn];//dp关键数组,d_max[i]表示从i出发的最长路,存储的是边
int d_min[maxn*maxn];//dp关键数组,d_min[i]表示从i出发的最短路,存储的是边
int path[maxn*maxn];//打印所有路径的时候需要
int scheme_max[maxn*10][maxn];//记录所有的最多硬币方案数,假设最多方案不超过1000个
int scheme_cnt_max;//最多硬币方案总数
int scheme_min[maxn*10][maxn];//记录所有的最少硬币方案数,假设最多方案不超过1000个
int scheme_cnt_min;//最少硬币方案总数

void read(){
    cout<<"硬币种类总数:";
    cin>>n;
    cout<<"各类硬币面值分别是:";
    for(int i=0;i<n;i++){
        cin>>coin[i];
    }
    cout<<"目标钱数:";
    cin>>S;
}

//求最大硬币数
int dp_max(int i){
    int &ans=d_max[i];
    if(ans!=-1) return ans;//注意初始化d_max数组为-1,表示尚未计算过,d_max[0]=0,
        //也可以开个vis数组进行标记:if(vis[i]) return ans; vis[i]=1;
        //紫书:如果状态复杂,用map存放状态值,判断状态i是否算过,只需要if(d.count(i))即可
    ans=-(1<<30);//注意这里初始化为一个很小的值,一是用来区分尚未做过的-1,二是可以表示如果无解的情况
            //注意这里ans一定不能初始化为0,否则当无解的时候,会返回一个错误的结果
    for(int j=0;j<n;j++){
        if(i>=coin[j]){
            ans=max(ans,dp_max(i-coin[j])+1);
        }
    }
    return ans;
}

//求最小硬币数
int dp_min(int i){
    int &ans=d_min[i];
    if(ans!=-1) return ans;
    ans=(1<<30);
    for(int j=0;j<n;j++){
        if(i>=coin[j]){
            ans=min(ans,dp_min(i-coin[j])+1);
        }
    }
    return ans;
}

//打印硬币数最大时候的字典序最小的路径,打印的是所有边,边是硬币面值
void print_min1(int i){
    for(int j=0;j<n;j++){
        if(i>=coin[j] && d_max[i]==d_max[i-coin[j]]+1){
            cout<<coin[j]<<" ";
            print_min1(i-coin[j]);
            break;
        }
    }
}

//打印从S状态出发的满足最多硬币的"所有方案"(按字典序从小到大打印,
//这里说的所有方案是指在图中走的路径不同,有可能多种方案其实是一种方案,
//因为如果多个方案对应的每种硬币的使用数目都相同的话,其实就是一种方案
void print_max_all(int i,int cnt){
    if(0==i){

        //这打印的是图中的所有路径,并非是真的方案
        for(int ii=0;ii<cnt;ii++){
            cout<<coin[path[ii]]<<" ";
        }
        cout<<endl;

        memset(coin_cnt,0,sizeof(coin_cnt));
        for(int k=0;k<cnt;k++){
            coin_cnt[path[k]]++;
        }
        //判断之前的方案中是否已经存在和本方案相同的方案,若存在返回true
        bool flag=false;//注意这里初始化是false
        for(int k=0;k<scheme_cnt_max;k++){
            flag=true;
            for(int kk=0;kk<n;kk++){
                if(coin_cnt[kk]!=scheme_max[k][kk]){
                    flag=false;
                    break;
                }
            }
            if(flag) break;
        }
        if(!flag){
            for(int kk=0;kk<n;kk++){
                scheme_max[scheme_cnt_max][kk]=coin_cnt[kk];
            }
            scheme_cnt_max++;
            /*
            for(int k=0;k<n;k++){
                cout<<coin_cnt[k]<<" ";
            }
            cout<<endl;
            */
        }

        return;
    }
    for(int j=0;j<n;j++){
            //如果把d_max[i]==d_max[i-coin[j]]+1去掉,相当于dfs了
        if(i>=coin[j] && d_max[i]==d_max[i-coin[j]]+1){
            path[cnt]=j;
            print_max_all(i-coin[j],cnt+1);
        }
    }
}


//打印硬币数最小时候的字典序最小的路径,打印的是所有边,边是硬币面值
void print_min2(int i){
    for(int j=0;j<n;j++){
        if(i>=coin[j] && d_min[i]==d_min[i-coin[j]]+1){
            cout<<coin[j]<<" ";
            print_min2(i-coin[j]);
            break;
        }
    }
}

//打印满足最多硬币的所有路径,顺便求出所有方案,必须先调用这个函数,才能打印所有方案
//打印从S状态出发的满足最少硬币的"所有方案"(按字典序从小到大打印,
//这里说的所有方案是指在图中走的路径不同,有可能多种方案其实是一种方案,
//因为如果多个方案对应的每种硬币的使用数目都相同的话,其实就是一种方案
void print_min_all(int i,int cnt){
    if(0==i){

        //这打印的是图中的所有路径,并非是真的方案
        for(int ii=0;ii<cnt;ii++){
            cout<<coin[path[ii]]<<" ";
        }
        cout<<endl;

        memset(coin_cnt,0,sizeof(coin_cnt));
        for(int k=0;k<cnt;k++){
            coin_cnt[path[k]]++;
        }
        //判断之前的方案中是否已经存在和本方案相同的方案,若存在返回true
        bool flag=false;//注意这里初始化是false
        for(int k=0;k<scheme_cnt_min;k++){
            flag=true;
            for(int kk=0;kk<n;kk++){
                if(coin_cnt[kk]!=scheme_min[k][kk]){
                    flag=false;
                    break;
                }
            }
            if(flag) break;
        }
        if(!flag){
            for(int kk=0;kk<n;kk++){
                scheme_min[scheme_cnt_min][kk]=coin_cnt[kk];
            }
            scheme_cnt_min++;
            /*
            for(int k=0;k<n;k++){
                cout<<coin_cnt[k]<<" ";
            }
            cout<<endl;
            */
        }

        return;
    }
    for(int j=0;j<n;j++){
            //如果把d_max[i]==d_max[i-coin[j]]+1去掉,相当于dfs了
        if(i>=coin[j] && d_min[i]==d_min[i-coin[j]]+1){
            path[cnt]=j;
            print_min_all(i-coin[j],cnt+1);
        }
    }
}


int main()
{
    memset(d_max,-1,sizeof(d_max));
    memset(d_min,-1,sizeof(d_min));
    d_max[0]=d_min[0]=0;
    read();
    int max_coin=dp_max(S);
    int min_coin=dp_min(S);
    if(min_coin==(1<<30) || max_coin<0){
        cout<<"无解"<<endl;
        return 0;
    }else{
        cout<<"---------------------------------------------------------"<<endl;
        cout<<"最大硬币数:"<<max_coin<<endl;
        cout<<"最大硬币数对应的字典序最小的路径(每个硬币的面值):";
        print_min1(S);
        cout<<endl;
        cout<<"最大硬币数的所有路径(从0到n-1每种硬币数目):"<<endl;
        print_max_all(S,0);
        cout<<"最大硬币数的所有方案(每个硬币的面值):"<<endl;
        for(int i=0;i<scheme_cnt_max;i++){
            for(int j=0;j<n;j++){
                cout<<scheme_max[i][j]<<" ";
            }
            cout<<endl;
        }

        cout<<"--------------------------------------------------------"<<endl;
        cout<<"最小硬币数:"<<min_coin<<endl;
        cout<<"最小硬币数对应的字典序最小的路径(每个硬币的面值):";
        print_min2(S);
        cout<<endl;
        cout<<"最小硬币数的所有路径(从0到n-1每种硬币数目):"<<endl;
        print_min_all(S,0);
        cout<<"最小硬币数的所有方案(每个硬币的面值):"<<endl;
        for(int i=0;i<scheme_cnt_min;i++){
            for(int j=0;j<n;j++){
                cout<<scheme_min[i][j]<<" ";
            }
            cout<<endl;
        }

    }

    return 0;
}

这里写图片描述

使用递推法,同时记录最小字典序路径

#include<iostream>
#include<cstring>
using namespace std;

const int maxn=105;
const int INF=(1<<30);

int coin[maxn];
int S;
int n;
int d_max[maxn*maxn];
int d_min[maxn*maxn];
int path_max[maxn];//记录最长路
int path_min[maxn];//记录最短路


void read(){
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>coin[i];
    }
    cin>>S;
}

void dp(){
    //初始化,如果最后需要的硬币数是个很大的数或很小的数,说明无解
    for(int i=0;i<=S;i++){
        d_max[i]=-INF;
        d_min[i]=INF;
    }
    d_max[0]=d_min[0]=0;//从0状态到0状态的硬币数是0,这个很关键
    for(int i=0;i<=S;i++){
        for(int j=0;j<n;j++){
            if(i>=coin[j]){
                if(d_max[i]<d_max[i-coin[j]]+1){
                    d_max[i]=d_max[i-coin[j]]+1;
                    path_max[i]=j;//同时记录路径
                }
                if(d_min[i]>d_min[i-coin[j]]+1){
                    d_min[i]=d_min[i-coin[j]]+1;
                    path_min[i]=j;
                }
            }
        }
    }
}



void print_path_max(int i){
    while(i){
        cout<<coin[path_max[i]]<<" ";
        i-=coin[path_max[i]];
    }
    cout<<endl;
}

void print_path_min(int i){
    while(i){
        cout<<coin[path_min[i]]<<" ";
        i-=coin[path_min[i]];
    }
    cout<<endl;
}


int main()
{
    read();
    dp();
    cout<<d_max[S]<<endl;
    cout<<d_min[S]<<endl;
    print_path_max(S);
    print_path_min(S);
    return 0;
}

这里写图片描述

使用状态转移方程d(i)=max{d(j)+1 | (j,i)∈E}加记忆化搜索求最大硬币数

因为上面已经详细求解了最大硬币和最小硬币数,并详细打印了方案和路径,这里仅仅以求最大硬币数为例,展示使用第二个状态转移方程的应用,一样可以解决问题。不过值得注意的事情是,有的题目两个状态转移方程都很容易求解,但有的问题要选择其中一个容易求解的状态转移方程进行求解。

#include<iostream>
#include<cstring>
using namespace std;

const int maxn=105;

int coin[maxn];
int S;
int n;
int d_max[maxn];


void read(){
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>coin[i];
    }
    cin>>S;
}


int dp(int i){
    int & ans=d_max[i];
    if(ans!=-1) return ans;
    ans=-(1<<30);
    for(int j=0;j<n;j++){
        if(i+coin[j]<=S){
            ans=max(ans,dp(i+coin[j])+1);
        }
    }
    return ans;
}

void print_min1(int i){
    for(int j=0;j<n;j++){
        if(i+coin[j]<=S && d_max[i]==d_max[i+coin[j]]+1){
            cout<<coin[j]<<" ";
            print_min1(i+coin[j]);
            break;
        }
    }
}


int main()
{
    memset(d_max,-1,sizeof(d_max));
    read();
    d_max[S]=0;//之前在没有读入S时,进行了此步,导致迷之错误
    dp(0);
    cout<<d_max[0]<<endl;
    print_min1(0);
    return 0;
}

这里写图片描述

猜你喜欢

转载自blog.csdn.net/ccnuacmhdu/article/details/81151964
今日推荐