算法竞赛入门经典【第九章 动态规划】习题1-10

习题1 Longest Run on a Snowboard UVA - 10285 (记忆化)

题目链接
搜索题单讲解

#include<bits/stdc++.h>
using namespace std;
const int N=110;
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int dp[N][N],h[N][N];
int r,c;
int DP(int x,int y){
    if(dp[x][y])return dp[x][y];
    int fl=1;
    for(int i=0;i<4;i++){
        int nx=x+dx[i],ny=y+dy[i];
        if(nx<1||ny<1||nx>r||ny>c)continue;
        if(h[nx][ny]<h[x][y]){
            fl=0;//当前不是最小的位置
            dp[x][y]=max(DP(nx,ny)+1,dp[x][y]);
        }
    }
    if(fl)dp[x][y]=1;
    return dp[x][y];
}
int main(){
    int T;cin>>T;
    while(T--){
        string name;
        cin>>name>>r>>c;
        memset(dp,0,sizeof dp);
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;j++)
                cin>>h[i][j];
        }
        int ans=0;
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;j++)
                ans=max(ans,DP(i,j));
        }
        cout<<name<<": "<<ans<<endl;
    }
    return 0;
}

习题2 Free Candies UVA - 10118 (记忆化)

题目链接
参考题解
状态设计: 现在有4堆糖果,每次只有4种选择,从4堆糖果中的哪堆拿1个糖果。原来我设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示现在在第 i i i次从第 j j j堆糖果拿的最大答案,但是发现这样不够表示状态,因为在拿第 j j j堆的时候还需要知道它现在最顶端的位置,因为 n n n为40,足够开4维,用 d p [ k 1 ] [ k 2 ] [ k 3 ] [ k 4 ] dp[k1][k2][k3][k4] dp[k1][k2][k3][k4]表示取到第i堆糖果已经取了 k i k_i ki颗时的最大答案。
状态转移: 用记忆化搜索,因为 k 1 , k 2 , k 3 , k 4 k1,k2,k3,k4 k1,k2,k3,k4的顺序无法确定,记忆化搜索可以解决无序的状态转移

  1. 篮子已经满了,已经不能再拿糖果,这个状态也不能向别的状态转移,答案也不会增加,直接得到0.
  2. 篮子未满,新加的糖果颜色在原来的篮子中出现过:将该颜色糖果从篮子里拿走。
  3. 篮子未满,新加的糖果颜色在原来的篮子中未出现过:直接加入该颜色。
#include<bits/stdc++.h>
using namespace std;
const int N=50;
int color[4][N], dp[N][N][N][N], n;
int solve(int num[],int basket[],int cnt){
    int &ans=dp[num[0]][num[1]][num[2]][num[3]];
    if(ans!=-1)return ans;
    ans=0;
    if(cnt==5)return ans;
    for(int i=0;i<4;i++){
        if(num[i]>=n)continue;
        int cur=color[i][num[i]];
        if(basket[cur]){
            basket[cur]=0,num[i]++;
            ans=max(ans,solve(num,basket,cnt-1)+1);
            basket[cur]=1,num[i]--;
        }
        else{
            basket[cur]=1,num[i]++;
            ans=max(ans,solve(num,basket,cnt+1));
            basket[cur]=0,num[i]--;
        }
    }
    return ans;
}
int main(){
    while(cin>>n&&n){
        for(int j=0;j<n;j++)for(int i=0;i<4;i++)cin>>color[i][j];
        memset(dp,-1,sizeof dp);
        int basket[21]={0},num[4]={0};
        cout<<solve(num,basket,0)<<endl;
    }
    return 0;
}

习题3 Cake slicing UVA - 1629 (记忆化)

题目链接
每次可以在一块蛋糕上面切1刀,那块蛋糕会被分成两块,只要每块上面还有樱桃就是合法的,假设切的蛋糕长和宽为 x , y x,y x,y,可以横切和纵切,一共有 x − 1 + y − 1 x-1+y-1 x1+y1种切法,将当前的蛋糕从蛋糕集合里删除,再加入两块新的切好的蛋糕继续切。还是采用记忆化搜索,因为切好的蛋糕顺序不定。最终状态是整个蛋糕都切好,起始状态是某块🎂只有1个🍒的时候,不用再切,直接返回0。
状态设计: dp[a][b][c][d]表示以(a,b)为左上角坐标,(c,d)为右下角坐标的矩形达到其中每小块只有1个樱桃至少需要切的长度。但是矩形除了坐标还需要知道其中的樱桃个数,只需要用一个二维前缀和数组就可以在O(1)时间计算某个矩形的樱桃个数以确定是否需要继续切割。
状态转移:

  1. 当前蛋糕只有1个樱桃,返回0。
  2. 枚举当前蛋糕横切和纵切。
#include<bits/stdc++.h>
using namespace std;
const int N=25;
const int inf=0x3f3f3f3f;
int dp[N][N][N][N],g[N][N],sum[N][N];//dp[lx][ly][rx][ry]为该矩形切割完成后的最少切割长度,num表示该矩形内的樱桃数量
int n,m,cherry;
inline int cal(int a,int b,int c,int d){
    return sum[c][d]-sum[a-1][d]-sum[c][b-1]+sum[a-1][b-1];
}
int DP(int a,int b,int c,int d){
    int &ans=dp[a][b][c][d];
    if(ans!=inf)return ans;
    if(cal(a,b,c,d)==1)return ans=0;
    for(int i=a;i<c;i++)//横切
        if(cal(a,b,i,d)&&cal(i+1,b,c,d))
            ans=min(ans,DP(a,b,i,d)+DP(i+1,b,c,d)+d-b+1);
    for(int i=b;i<d;i++)
        if(cal(a,b,c,i)&&cal(a,i+1,c,d))
            ans=min(ans,DP(a,b,c,i)+DP(a,i+1,c,d)+c-a+1);
    return ans;
}
int main(){
    int kase=0;
    while(cin>>n>>m>>cherry&&n&&m){
        memset(dp,0x3f,sizeof dp);
        memset(sum,0,sizeof sum);
        memset(g,0,sizeof g);
        for(int i=1;i<=cherry;i++){
            int x,y;cin>>x>>y;
            g[x][y]=1;
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
                if(g[i][j])sum[i][j]++;
            }
        }
        cout<<"Case "<<++kase<<": ";
        cout<<DP(1,1,n,m)<<endl;
    }
    return 0;
}

习题4 Cyborg Genes UVA - 10723

题目链接
参考代码

状态设计:
dp1[i][j]表示第一个到第一个串的i位置和第二个串的j位置时,两个串前缀的最大公共子序列。
dp1[i][j]表示第一个到第一个串的i位置和第二个串的j位置时,两个串前缀的最多数量。
第一个状态比较容易想,是LCS(最大公共子序列)的简单变形,要使得后来的新串既包含原来两个串,又要使新串最短,那新串就是在原来两个串的LCS里穿插两个串特有的其他非公共序列。

状态转移:
第二个状态dp2[i][j]完全依赖于第一个状态的转移情况:

  1. 当前位置两个字符相同,dp2[i][j]=dp2[i-1][j-1],因为现在的LCS就是加上当前位置相同的字符s1[i-1]和s2[j-1](因为是从0开始计数,字符串位置是当前dp枚举位置-1)
#include<bits/stdc++.h>
const int inf=0x3f3f3f3f;
using namespace std;
const int N=40;
int dp1[N][N],dp2[N][N];
string s1,s2;
int main(){
    int T;cin>>T;
    int kase=0;
    getchar();
    while(T--){
        getline(cin,s1),getline(cin,s2);
        memset(dp1,0,sizeof dp1);
        memset(dp2,0,sizeof dp2);
        int n1=s1.length(),n2=s2.length();
        for(int i=0;i<=n1;i++)dp2[i][0]=1;
        for(int i=0;i<=n2;i++)dp2[0][i]=1;
        for(int i=1;i<=n1;i++){
            for(int j=1;j<=n2;j++){
                if(s1[i-1]==s2[j-1]){
                    dp1[i][j]=dp1[i-1][j-1]+1;
                    dp2[i][j]=dp2[i-1][j-1];
                }
                else {
                    dp1[i][j]=max(dp1[i-1][j],dp1[i][j-1]);
                    if(dp1[i-1][j]>dp1[i][j-1])
                        dp2[i][j]+=dp2[i-1][j];
                    else if(dp1[i][j-1]>dp1[i-1][j])
                        dp2[i][j]+=dp2[i][j-1];
                    else dp2[i][j]+=dp2[i-1][j]+dp2[i][j-1];
                }
            }
        }
        cout<<"Case #"<<++kase<<": "<<n1+n2-dp1[n1][n2]<<' '<<dp2[n1][n2]<<endl;
    }
}

习题8 Alibaba UVA - 1632 (区间DP)

虽然觉得时间空间真的宽容到离谱
题目链接
状态设计: 考虑一个区间 ( l , r ) (l,r) (l,r),当这个区间里的宝藏都被取走,最后肯定从 l l l离开或者从 r r r离开。因此一个区间还要继续分成两个状态: d p [ l ] [ r ] [ 0 ] dp[l][r][0] dp[l][r][0]表示从 l l l离开区间, d p [ l ] [ r ] [ 1 ] dp[l][r][1] dp[l][r][1]表示从 r r r离开此区间。
状态转移: d p [ l ] [ r ] [ 0 ] = m i n ( d p [ l + 1 ] [ r ] [ 0 ] + d i s ( l , l + 1 ) , d p [ l + 1 ] [ r ] [ 1 ] + d i s [ l , r ] ) dp[l][r][0]=min(dp[l+1][r][0]+dis(l,l+1),dp[l+1][r][1]+dis[l,r]) dp[l][r][0]=min(dp[l+1][r][0]+dis(l,l+1),dp[l+1][r][1]+dis[l,r])
dp[l][r][0]转移同理。

#include<bits/stdc++.h>
#define fD from+Dis
#define min3(a,b,c) min(a,min(b,c))
const int inf=0x3f3f3f3f;
using namespace std;
const int N=1e4+7;
int dp[N][N][2];
int p[N],d[N];
int n;
inline int dis(int x,int y){return abs(p[x]-p[y]);}
int main(){
    while(cin>>n){
        for(int i=1;i<=n;i++){
            cin>>p[i]>>d[i];
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++)
                dp[i][j][0]=dp[i][j][1]=inf;
        }
        for(int i=1;i<=n;i++)dp[i][i][0]=dp[i][i][1]=0;
        for(int len=2;len<=n;len++){//len表示区间内有几个点
            for(int i=1;i<=n-len+1;i++){
                int l=i,r=i+len-1;
                
                int from=dp[l+1][r][0],Dis=dis(l,l+1);
                if(d[l]>fD)dp[l][r][0]=fD;
                
                from=dp[l+1][r][1],Dis=dis(l,r);
                if(d[l]>fD)dp[l][r][0]=min(dp[l][r][0],fD);
                
                from=dp[l][r-1][1],Dis=dis(r-1,r);
                if(d[r]>fD)dp[l][r][1]=min(dp[l][r][1],fD);
                
                from=dp[l][r-1][0],Dis=dis(l,r);
                if(d[r]>fD)dp[l][r][1]=min(dp[l][r][1],fD);
            }
        }
        if(dp[1][n][0]!=inf||dp[1][n][1]!=inf)
        cout<<min(dp[1][n][0],dp[1][n][1])<<endl;
        else cout<<"No solution"<<endl;
    }
}


习题9 Storage Keepers UVA - 10163

题目链接
参考博客
状态设计:
dp1[i][j]表示前i个仓库用了前j个守卫时最小安全系数的最大值。
dp2[i][j]表示前i个仓库用了前j个守卫时最小的花费。
状态转移: 外层枚举守卫,内层枚举仓库。多加一个守卫会贡献守卫值,这个守卫值可以去分摊给后几个人。

  1. min里第一个表示前k-1个中最小系数的最大值,第二个表示雇佣第i个守卫看守k个仓库的安全系数:
    d p 1 [ j ] = m a x ( d p 1 [ j ] , m i n ( d p 1 [ j − k ] , p [ i ] / k ) ) dp1[j]=max(dp1[j],min(dp1[j-k],p[i]/k)) dp1[j]=max(dp1[j],min(dp1[jk],p[i]/k))
  2. 取工资最小值。
    d p 2 [ j ] = m i n ( d p 2 [ j ] , d p 2 [ j − k ] + p [ i ] ) ; dp2[j]=min(dp2[j],dp2[j-k]+p[i]); dp2[j]=min(dp2[j],dp2[jk]+p[i]);
#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
int dp[110];
int m,n;//m为仓库 n为守卫个数
int p[40];
int main(){
    while(cin>>m>>n&&n&&m){
        memset(dp,0,sizeof dp);
        dp[0]=inf;
        for(int i=1;i<=n;i++)cin>>p[i];
        //先计算最小安全系数的最大值
        for(int i=1;i<=n;i++){      //枚举守卫
            for(int j=m;j>=0;j--){     //枚举仓库
                for(int k=1;k<=j&&k<=p[i];k++){ //枚举第i名守卫看守的仓库个数
                    dp[j]=max(dp[j],min(dp[j-k],p[i]/k));
                }
            }
        }
        int ans=dp[m];
        cout<<ans<<' ';
        if(!ans){
            cout<<ans<<endl;continue;
        }
        memset(dp,0x3f,sizeof dp);
        dp[0]=0;
        for(int i=1;i<=n;i++){
            for(int j=m;j>=0;j--){
                for(int k=min(j,p[i]/ans);k>0;k--){
                    dp[j]=min(dp[j],dp[j-k]+p[i]);
                }
            }
        }
        cout<<dp[m]<<endl;
    }
    return 0;
}


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值