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

例1 A Spy in the Metro UVA - 1025

状态设计:dp[i][j]表示在第i时刻在第j个站点至少还需要等待的时长。
状态转移
在当前的车站等待;
坐上向左开的车;
坐上向右开的车。
枚举顺序:第二维的枚举顺序没有关系 因为对于每个j,dp[i+1][j]都已经更新好,最重要的是第一维有序的时间枚举,这也是递推的顺序,即当前时间的状态是从前1秒转移而来。

#include<bits/stdc++.h>
using namespace std;
int n,T,t[100],m1,m2,d1[100],d2[100];
bool has_train[300][100][2];//has_train[i][j][0]表示在第i时刻第j个车站是否有向右边开的列车
int dp[300][100];//dp[i][j]表示在第i时刻在第j个站点至少还需要等待的时长
void solve(){
    memset(has_train,0,sizeof has_train);
    memset(dp,0x3f,sizeof dp);
    cin>>T;
    dp[T][n]=0;//已经在此处会面 等待时间为0
    for(int i=1;i<n;i++)cin>>t[i];
    cin>>m1;
    for(int i=1;i<=m1;i++){
        cin>>d1[i];int cur=d1[i];
        for(int j=1;j<n;j++){
            if(cur>T)break;
            has_train[cur][j][0]=1;
            cur+=t[j];
        }
    }
    cin>>m2;
    for(int i=1;i<=m2;i++){
        cin>>d2[i];int cur=d2[i];
        for(int j=n;j>1;j--){
            if(cur>T)break;
            has_train[cur][j][1]=1;
            cur+=t[j-1];
        }
    }

    for(int i=T-1;i>=0;i--){
        for(int j=1;j<=n;j++){
            dp[i][j]=min(dp[i][j],dp[i+1][j]+1);
            if(has_train[i][j][0]&&i+t[j]<=T&&j+1<=n)
                dp[i][j]=min(dp[i][j],dp[i+t[j]][j+1]);
            if(has_train[i][j][1]&&j-1>=1&&i+t[j-1]<=T)
                dp[i][j]=min(dp[i][j],dp[i+t[j-1]][j-1]);
        }
    }
}
int main(){
    int kase=0;
    while(cin>>n&&n){
        solve();
        cout<<"Case Number "<<++kase<<": ";
        if(dp[0][1]!=0x3f3f3f3f)
        cout<<dp[0][1]<<endl;
        else cout<<"impossible"<<endl;
    }
    return 0;
}

例2 The Tower of Babylon UVA - 437

每个立方体有6种可以摆放的角度,其实这就是一个最长上升子序列的三维问题。
状态设计:dp[i]表示以编号为i顶端三角形的最大高度
状态转移:dp[i]=max(dp[j]+node[i].z,dp[i])。其中j表示所有底面长宽小于i的立方体编号
代码样例都过不了…在我的理解,无法通过的原因是:当用j去更新i的时候,并无法保证j已经是以j为顶的最大高度,因为排序是无效的,比如a<b,a<c,b<d,c<e,但是无法保证b<e,c<d。

//错误代码!!
#include<bits/stdc++.h>
using namespace std;
struct Node{
    int x,y,z;
    bool operator<(const Node &a){
        if(x<a.x&&y<a.y)return true;
        if(x<a.y&&y<a.x)return true;
        return false;
    }
}node[100];
int dp[100],n;
bool cmp(Node a,Node b){return b<a;}
int solve(){
    memset(dp,0,sizeof dp);
    for(int i=1;i<=n;i++){
        int x,y,z;cin>>x>>y>>z;
        node[i]={x,y,z},node[i+n]={y,z,x},node[i+2*n]={x,z,y};
    }
    int ans=0;
    sort(node+1,node+1+n*3,cmp);
    for(int i=1;i<=3*n;i++)dp[i]=node[i].z;
    for(int i=1;i<=3*n;i++){
        for(int j=1;j<=3*n;j++){//如何保证j已经更新完毕呢!即dp[j]表示以j为顶端三角形的最大高度
            if(node[i]<node[j])dp[i]=max(dp[j]+node[i].z,dp[i]);
        }
    }
    for(int i=1;i<=3*n;i++)ans=max(dp[i],ans);
    return ans;
}
int main(){
    int kase=0;
    while(cin>>n&&n){
        int ans=solve();
        cout<<"Case "<<++kase<<": maximum height = "<<ans<<endl;
    }
}

参考题解1
正解是DAG上的动态规划。
状态设计: dp[i]表示以i为顶的柱子的最大高度
状态转移: 更新dp[u]时,枚举比u小的立方体i,进行递归计算,dp[i]用的是所有已经到达过最底层的状态去更新的,所以dp[u]一旦算出就是最大值。

#include<bits/stdc++.h>
using namespace std;
const int N=1e2+10;
int n;
int dp[N];
struct Node{
    int x,y,z;
    bool operator<(const Node &a){
        if(x<a.x&&y<a.y)return true;
        if(x<a.y&&y<a.x)return true;
        return false;
    }
}node[N];
int DP(int u){
    if(dp[u])return dp[u];
    for(int i=1;i<=n;i++){
        if(i==u)continue;
        if(node[u]<node[i]){
           int update=DP(i);
           if(update>dp[u])dp[u]=update;
        }
    }
    return dp[u]=dp[u]+node[u].z;
}
int solve(){
    memset(dp,0,sizeof dp);
    for(int i=1;i<=n;i++){
        int x,y,z;cin>>x>>y>>z;
        node[i]={y,z,x},node[i+n]={x,z,y},node[i+2*n]={x,y,z};
    }
    n*=3;
    int ans=0;
    for(int i=1;i<=n;i++){
        ans=max(ans,DP(i));
    }
    return ans;
}
int main(){
    int kase=0;
    while(cin>>n&&n){
        int ans=solve();
        cout<<"Case "<<++kase<<": maximum height = ";
        cout<<ans<<endl;
    }
    return 0;
}

参考题解2

状态设计: dp[i][j]表示以i为底,以j为顶的柱子的最大高度
状态转移: 枚举比i小,比j大的立方体,尝试用dp[i][k]再叠放j立方体的方法更新dp[i][j]。其实很像区间dp,但是这里的区间是有序的。因为dp[i][j]用的是所有已经到达过最底层的状态去更新的,所以dp[i][j]一旦算出就是最大值。

int dp[N][N];//dp[i][j]:以i为底以j为顶的最大值
int DP(int down,int top){
    int &ans=dp[down][top];
    if(ans)return ans;
    for(int i=1;i<=n;i++){
        if(i==down)continue;
        if(node[i]<node[down]&&node[top]<node[i])
        ans=max(ans,DP(down,i)+node[top].z);
    }
    if(!ans)return node[top].z+node[down].z;
    return ans;
}

例3 Tour UVA - 1347

状态设计: dp[i][j]表示第1个人走到第i个点,第2个人走到第j个点时两个人还需要走的距离,且此时1-max(i,j)都被走过了,且设定i>j。

状态转移:
第一个人从i位置走到i+1的位置:
d p [ i ] [ j ] = m i n ( d p [ i + 1 ] [ j ] + n o d e [ i ] . d i s ( n o d e [ i + 1 ] ) , d p [ i ] [ j ] ) ; dp[i][j]=min(dp[i+1][j]+node[i].dis(node[i+1]),dp[i][j]); dp[i][j]=min(dp[i+1][j]+node[i].dis(node[i+1]),dp[i][j]);
第二个人从j位置走到i+1位置:
d p [ i ] [ j ] = m i n ( d p [ i + 1 ] [ i ] + n o d e [ j ] . d i s ( n o d e [ i + 1 ] ) , d p [ i ] [ j ] ) ; dp[i][j]=min(dp[i+1][i]+node[j].dis(node[i+1]),dp[i][j]); dp[i][j]=min(dp[i+1][i]+node[j].dis(node[i+1]),dp[i][j]);
为什么是i+1?
每次都选择一个人走到i+1位置,才能保证从1到i+1每个点都恰好有一个人经过。
第二个状态转移的合理性?
显然 d p [ i ] [ j ] = d p [ j ] [ i ] dp[i][j]=dp[j][i] dp[i][j]=dp[j][i]

用定义解释即:"第一个人走到i位置且第二个人走到j位置"的状态 与"第二个人走到i位置且第一个人走到j位置"的状态 其实是同一件事,完全是具有对称性的。回到状态转移方程:”第二个人从j走到i+1,而第一个人留在原地i“ 的状态与 “第二个人(相当于交换位置但不花费距离)去到i,而第一个人去到i+1”的状态 表达的是相同的意思。这么做是为了保持dp第一维总是大于第二维,简化状态转移过程。

枚举顺序: 确定起始状态dp[n-1][n]与dp[n-1][n]后,目标状态为dp[2][1](记住我们已经确定第一维大于第二维)再加上dis(1,2)。第一维的枚举顺序应该从大到小,第二维枚举顺序无关紧要。因为是第一维i+1所有状态更新之后才去更新第一维i的,此时对于所有的j,dp[i+1][j]已经更新完毕。
tips: 枚举顺序由状态转移方程决定,明确已知状态和待求状态。

#include<bits/stdc++.h>
using namespace std;
const int N=1E3+10;
int n;
double dp[N][N];
struct Node{
    int x,y;
    double dis(const Node &a){return sqrt((a.x-x)*(a.x-x)+(a.y-y)*(a.y-y));}
}node[N];
void solve(){
    for(int i=1;i<=n;i++)cin>>node[i].x>>node[i].y;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dp[i][j]=1e18;
    dp[n][n]=0,dp[n-1][n]=dp[n][n-1]=node[n-1].dis(node[n]);
    for(int i=n-1;i>=1;i--){
        for(int j=i;j>=1;j--){
            dp[i][j]=min(dp[i+1][j]+node[i].dis(node[i+1]),dp[i][j]);
            dp[i][j]=min(dp[i+1][i]+node[j].dis(node[i+1]),dp[i][j]);
        }
    }
    printf("%.2lf\n",dp[2][1]+node[1].dis(node[2]));
}
int main(){
    while(cin>>n)
        solve();
    return 0;
}

例4 Unidirectional TSP UVA - 116

状态设计: dp[i][j]表示从位置(i,j)出发到达最后1列的最小权值和。起始状态是在最后一列,目标状态是在第一列。
状态转移: 共3个方向,向上或向下或向同行右侧转移。
其他:这道题还需要记录字典序最小的答案,因此每次更新答案时需要记录路径,用一个nxt[i][j]数组记录更新时每个位置可以向哪个方向转移。

#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=1E3+10;
int dp[12][110];
int n,m;
int nxt[12][110];
int a[12][110];
void solve(){
    memset(dp,0x3f,sizeof dp);
    int pos=1,ans=inf;
    //dp[i][j]表示到达i行j列位置的最小答案
    for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>a[i][j];
    for(int i=1;i<=n;i++)dp[i][m]=a[i][m];
    for(int j=m;j>=1;j--){//从最后1列开始逆推
        for(int i=1;i<=n;i++){
            int row[3]={i,i-1,i+1};//三个决策方向
            if(i==1)row[1]=n;
            if(i==n)row[2]=1;
            sort(row,row+3);
            for(int k=0;k<3;k++){
                //记录当前决策的上一个位置,相当于记录路径
                if(dp[i][j]>dp[row[k]][j+1]+a[i][j]){
                     dp[i][j]=dp[row[k]][j+1]+a[i][j];
                     nxt[i][j]=row[k];//保存行
                }
            }
            if(j==1&&dp[i][j]<ans){
                ans=dp[i][j];
                pos=i;
            }
        }
    }
    cout<<pos;
    int j=1;
    while(j<m){
        cout<<' '<<nxt[pos][j];
        pos=nxt[pos][j++];
    }cout<<endl;
    cout<<ans<<endl;
}
int main(){
    while(cin>>n>>m)
        solve();
    return 0;
}

例5 Jin Ge Jin Qu hao UVA - 12563

总之只要在最后1s唱劲歌金曲就好了。
状态设计: dp[i][j]表示到第i首歌且总的可以唱的时间为j,可以唱的最多歌曲数。
状态转移: 共3个方向,向上或向下或向同行右侧转移。
其他:这道题还需要所能唱的最长的时间,就是记录所有被选中的歌曲。我非常天真地将需要的时间从大到小排了序,恰好过了样例…直觉想的没有任何理论支撑,果不其然,wa了。

//错误代码!!!
#include<bits/stdc++.h>
using namespace std;
const int N=9700;
int kase,n,t;
int dp[60][N];
int a[60];
int vis[60];
bool cmp(int a,int b){return a>b;}
void solve(){
    memset(dp,0,sizeof dp);
    cin>>n>>t;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    sort(a+1,a+1+n,cmp);
    t--;
    int t_need=0,ans=0;
    for(int i=1;i<=n;i++){
        for(int j=0;j<=t;j++){
            dp[i][j]=dp[i-1][j];//不唱当前这首歌
            if(j>=a[i]){
                if(dp[i-1][j-a[i]]+1>dp[i][j]){
                    dp[i][j]=max(dp[i][j],dp[i-1][j-a[i]]+1);
                    if(ans<dp[i][j]){
                        ans=dp[i][j],t_need=j;
                    }
                }
            }
        }
    }
    cout<<"Case "<<++kase<<": "<<dp[n][t]+1<<' '<<t_need+678<<endl;
}
int main(){
    int T;cin>>T;
    while(T--)
        solve();
    return 0;
}

参考题解
原来是要用两个dp!(可以用滚动数组优化滚动数组的理解优化)
状态设计: 这题是多决策问题。因为需要两个决策,可以开一个二维数组(如果需要更多的决策可以考虑直接开一个结构体)。dp[i][0]表示可以唱总时长为i时最多可以唱的歌曲数目,dp[i][1]表示可以唱总时长为i时最多可以唱的歌曲时长。
状态转移:分两种情况,当转移后的歌曲数目不变时则考虑时长的转移。

#include<bits/stdc++.h>
using namespace std;
const int N=9700;
int kase,n,t;
int dp[N][2];
int a[60];
int vis[60];
void solve(){
    memset(dp,0,sizeof dp);
    cin>>n>>t;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    t--;
    for(int i=1;i<=n;i++){
        for(int j=t;j>=a[i];j--){
            if(dp[j][0]<dp[j-a[i]][0]+1){
                dp[j][0]=dp[j-a[i]][0]+1;
                dp[j][1]=dp[j-a[i]][1]+a[i];
            }
            else if(dp[j][0]==dp[j-a[i]][0]+1)
                dp[j][1]=max(dp[j][1],dp[j-a[i]][1]+a[i]);
        }
    }
    cout<<"Case "<<++kase<<": "<<dp[t][0]+1<<' '<<dp[t][1]+678<<endl;
}
int main(){
    int T;cin>>T;
    while(T--)
        solve();
    return 0;
}

例6 Another Crisis UVA - 12186

题目链接
到树形dp了!
状态设计:dp[i]表示i向上级递交请愿书需要的工人数量,对于工人而言就是他本身,也就是1。工人的状态是起始状态,都是1,目标状态是老板的dp[0]。
状态转移:求dp[[i],建树之后将i的孩子的dp都求出来,从小到大排序,选前k%求和。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10;
int n,k;
int dp[N];//dp[i]表示让i签字的最少工人数量
vector<int>son[N];
int DP(int u){
    int sz=son[u].size();
    if(!sz)return dp[u]=1;//工人
    if(dp[u])return dp[u];
    int cnt=sz*k/100;
    if(sz*k%100)cnt++;
    vector<int>son_dp;
    for(int i=0;i<sz;i++)son_dp.push_back(DP(son[u][i]));
    sort(son_dp.begin(),son_dp.end());
    for(int i=0;i<cnt;i++)dp[u]+=son_dp[i];
    return dp[u];
}
void solve(){
    memset(dp,0,sizeof dp);
    for(int i=0;i<=n;i++)son[i].clear();
    for(int i=1;i<=n;i++){
        int x;cin>>x;
        son[x].push_back(i);
    }
    cout<<DP(0)<<endl;
}
int main(){
    while(cin>>n>>k&&n&&k)
        solve();
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值