Atcoder DP contest 题解

动态规划(Atcoder DP 26题)

Frog 1#

N 个石头,编号为 1,2,...,N。对于每个 i(1iN),石头 i 的高度为 hi
最初有一只青蛙在石头 1 上。他将重复几次以下操作以到达石头 N
如果青蛙当前在石头 i 上,则跳到石头 i+1 或石头 i+2需要 |hihj| 的费用,而 j 是要落到上面的石头。
找到青蛙到达石头 N 之前需要的最小总费用。

  • 2N 105
  • 1hi 104

这是一个线性动态规划,在这题中,阶段被定义为青蛙正处在的格子,每次的决策就是跳 1 格或者 2 格。

定义:f[i] 表示跳到第 i 个格子的最小总费用。

转移:f[i]=min(f[i-1]+abs(h[i-1]-h[i]),f[i-2]+abs(h[i-2]-h[i]));

当然,i2 时需要特判。

Frog 2#

题意基本和上一题相同,不过这题能跳 K 格。我们只需把上一题的转移改成循环的格式即可。

Vacation#

太郎的暑假有n天,第i天他可以选择做以下三种事情:

  1. 游泳,获得ai点幸福值。
  2. 捉虫,获得bi点幸福值。
  3. 写作业,获得ci点幸福值。

但他不能连续两天进行同一种活动,请求出最多可以获得多少幸福值。

  • 1  N  105
  • 1  ai, bi, ci  104

很明显的阶段是天数,每天的决策也很简单,唯一限制我们的是“不能连续两天进行同一种活动”的要求,我们在动态规划的状态里还没表示出来。进一步发现,我们只需要相邻两天的活动,这启示我们给 f 数组加一维。

定义:f[i][0/1/2] 表示第 i 天,当天选择了某种活动的最大幸福值。

转移:if(j!=k) f[i][j]=max(f[i][j],f[i-1][k]+a[i][j]);

最终答案就是max{fn,0,fn,1,fn,2}

Knapsack 1#

01 背包问题。

Knapsack 2#

题意同上,但是数据范围:

  • 1  N  100
  • 1  W  109
  • 1  wi  W
  • 1  vi  103

如果把 W 作为动态规划的阶段显然空间复杂度过大,所以考虑把 W 作为动态规划的答案。

定义:f[i][j] 表示前 i 个物品得到 j 的价值所需最小重量。

转移:f[i][j]=min(f[i-1][j],f[i-1][j-v[i]]+w[i]);

统计答案时倒序枚举 j,当第一次出现 f[n][j]<=W 时,输出答案。

此题亦可压维:

for(int i=1;i<=n;i++)
    for(int j=sv;j>=v[i];j--)
        f[j]=min(f[j],f[j-v[i]]+w[i]);
for(int i=sv;i>=0;i--)
    if(f[i]<=W){
        cout<<i<<endl;break;
    }

LCS#

基础的 LCS 问题,区别是要输出一种方案,可以采取记录从哪里来的方法,最后逆序递归一次即可。

const int N=3005,M=(N<<1),inf=0x3f3f3f3f;
int n,m,f[N][N];
Pair p[N][N];
string x,y;
void print(int i,int j){
    Pair now=p[i][j];
    if(now.first&&now.second) print(now.first,now.second);
    if(i-now.first==1&&j-now.second==1)
        cout<<x[i-1];
}
int main(){
    ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin>>x>>y;
    n=x.size();m=y.size();
    F(i,1,n){
        F(j,1,m){
            f[i][j]=max(f[i-1][j],f[i][j-1]);
            if(f[i-1][j]>f[i][j-1]) p[i][j]={i-1,j};
            else p[i][j]={i,j-1};
            if(x[i-1]==y[j-1]) {
                if(f[i-1][j-1]+1>f[i][j]){
                    f[i][j]=f[i-1][j-1]+1;
                    p[i][j]={i-1,j-1};
                }
            }
        }
    }
    print(n,m);
    return 0;
}

Longest Path#

求有向无环图上的最长路长度。

做一个拓扑排序,顺便统计答案即可。

const int N=100005,M=(N<<1),inf=0x3f3f3f3f;
int n,m,outd[N],f[N];
vector<int> G[N];
int main(){
    ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin>>n>>m;
    F(i,1,m){
        int u,v;
        cin>>u>>v;
        G[v].push_back(u);
        outd[u]++;
    }
    queue<int> q;
    F(i,1,n)
        if(!outd[i])
            q.push(i);
    while(q.size()){
        int now=q.front();
        q.pop();
        for(auto i:G[now]){
            f[i] = max(f[i],f[now]+1);
            if(!(--outd[i])) q.push(i);
        }
    }
    cout<<*max_element(f+1,f+n+1);
    return 0;
}

Grid 1#

参见NOIP过河卒即可。

Coins#

期望DP。

定义:f[i][j]表示前 i 个硬币有 j 个正面朝上的概率。

转移:f[i][j]=f[i-1][j-1]*p[i]+f[i-1][j]*(1-p[i]);

初始化:f[0][0]=1;

Sushi#

这题写了题解。

Part 1 题意分析#

n 个盘子,每次随机选择一个盘子,吃掉其中的一个寿司。若没有寿司则不吃。

求最后吃掉所有寿司的期望步数。

题意告诉我们非常重要的一点,可以选择空盘子,也就是说这些浪费的步数也会算入期望里。

Part 2 状态转移方程#

定义:fi,j,k 表示有 i 个盘子里有一个寿司,j 个盘子里有两个寿司,k 个盘子里有三个寿司时吃完所有寿司的期望。

初始化:f0,0,0=0

接下来,列出最初的方程:

fi,j,k=nijkn×fi,j,k+in×fi1,j,k+jn×fi+1,j1,k+kn×fi,j+1,k1+1

首先看第一项,nijkn×fi,j,k 表示有 nijkn 的概率选到空盘子,吃完空盘子之后,会发现局面没有任何变化,现在想吃完所有寿司仍然需要 fi,j,k 的期望步数。

第二项,这次选择吃有一个寿司的盘子,吃完后下一步里有一个寿司的盘子就少了一个,到达了 fi1,j,k 的局面,选中这个盘子的概率同上,不再解释。

第三项,这次选择吃有两个寿司的盘子,吃完后发现,两个寿司的盘子少了一个,可是剩下的那个寿司就成为了一个寿司的盘子了,所以到达了 fi+1,j1,k 的局面。

最后解释一下最后一个加一的含义,注意到我前面特别标粗了吃完两个字,意思就是我们目前加的都是吃完后的期望,难道当前吃的这一步不需要步数吗?所以这个一表示的就是当前吃的这一步!

理解了初代方程之后,我们就可以合并同类项来化简了。

i+j+kn×fi,j,k=in×fi1,j,k+jn×fi+1,j1,k+kn×fi,j+1,k1+1

(i+j+k)×fi,j,k=i×fi1,j,k+j×fi+1,j1,k+k×fi,j+1,k1+n

fi,j,k=ii+j+k×fi1,j,k+ji+j+k×fi+1,j1,k+ki+j+k×fi,j+1,k1+ni+j+k

Part 3 代码#

这题如果需要循环转移的话需要按照 k,j,i 的顺序,相较之下记忆化搜索不用多想。

const int N=305,M=(N<<1),inf=0x3f3f3f3f;
double f[N][N][N];
double dp(int i,int j,int k){
    if(i==0&&j==0&&k==0) return 0;
    if(f[i][j][k]) return f[i][j][k];
    double p=n*1.0/(i+j+k);
    if(i) p+=i*1.0/(i+j+k)*dp(i-1,j,k);
    if(j) p+=j*1.0/(i+j+k)*dp(i+1,j-1,k);
    if(k) p+=k*1.0/(i+j+k)*dp(i,j+1,k-1);
    return f[i][j][k]=p;
}
int main(){
    ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    int n,x,c[4]={0};cin>>n;
    F(i,1,n){
        cin>>x;
        c[x]++;
    }
    cout<<fixed<<setprecision(10)<<dp(c[1],c[2],c[3]);
    return 0;
}

Stones#

可行性DP。

定义:f[i]表示 i 个石头是先手还是后手赢。

一句话转移:F(i,0,m)F(j,1,n) f[i+a[j]]+=!f[i];

Deque#

区间DP。

定义:f[l][r]表示l到r之间的X-Y。

区间DP转移时有一个小转化,就是先手尽可能大就是X-Y大,后手的反而小。

dF(i,n,1) opt[i]=opt[i+1]^1;
F(len,1,n){
    for(int l=1,r=l+len-1;r<=n;l++,r++){
        if(opt[len]) f[l][r]=max(f[l][r-1]+a[r],f[l+1][r]+a[l]);
        else f[l][r]=min(f[l][r-1]-a[r],f[l+1][r]-a[l]);
    }
}

注意要判断当前是先手还是后手。

Candies#

前缀和优化 DP。

普通的定义:f[i][j] 表示前 i 个小朋友发 j 个糖的方案数量。

套路的转移:

F(i,1,n)F(j,0,m)
    F(k,max(j-a[i],0),j)
        f[i][j]+=f[i-1][k];

但是这样的话时间复杂度就超标了,注意到我们每次都是加上了连续的一段,那么就可以用前缀和优化掉。

F(i,1,n){
    F(j,1,m) (f[i-1][j]+=f[i-1][j-1])%=mod;
    F(j,0,m){
        if(max(0,j-a[i])>0) f[i][j]=((f[i-1][j]-f[i-1][max(0,j-a[i])-1])%mod+mod)%mod;
        else f[i][j]=f[i-1][j];
    }
}

注意负数取模。

作者:紊莫

出处:https://www.cnblogs.com/wenmoor/p/17980952

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   紊莫  阅读(51)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu