ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【杂题乱写】AtCoder dp 26题

2022-08-14 15:02:05  阅读:162  来源: 互联网

标签:AtCoder 26 乱写 复杂度 int maxn read main dp


AtCoder dp 26题

原比赛链接 洛谷题单链接

A - Frog 1

题目已然给出了转移方程,设 \(dp_i\) 为到第 \(i\) 块石头的最小代价。
转移方程:

\[dp_i=\min(dp_{i-1}+|h_i-h_{i-1}|,dp_{i-2}+|h_i-h_{i-2}|) \]

时间复杂度:\(O(n)\)

点击查看代码
int n;
int h[maxn],dp[maxn];
int main(){
    n=read();
    for(int i=1;i<=n;++i) h[i]=read();
    dp[1]=0,dp[2]=abs(h[1]-h[2]);
    for(int i=3;i<=n;++i){
        dp[i]=min(dp[i-2]+abs(h[i-2]-h[i]),dp[i-1]+abs(h[i-1]-h[i]));
    }
    print

B - Frog 2

多了一层步数的限制,枚举一下即可,\(dp_i\) 定义同上。
转移方程:

\[dp_i=\min_{j=i-k}^{i-1}\{dp_j+|h_i-h_j|\} \]

时间复杂度:\(O(nK)\)

点击查看代码
int n,k;
int h[maxn],dp[maxn];
int main(){
    n=read(),k=read();
    for(int i=1;i<=n;++i) h[i]=read(),dp[i]=maxxn; 
    dp[1]=0;
    for(int i=1;i<=n;++i){
        for(int j=i+1;j<=min(i+k,n);++j){
            dp[j]=min(dp[j],dp[i]+abs(h[i]-h[j]));
        }
    }
    printf("%d\n",dp[n]);
    return 0;
}   

C - Vacation

相当朴素,设 \(dp_{i,0/1/2}\) 分别表示第 \(i\) 天进行某种活动的最大收益,从第 \(i-1\) 天的两个不同状态转移即可。
时间复杂度:\(O(n)\)

点击查看代码
int n;
int a[maxn],b[maxn],c[maxn];
int dp[maxn][3];
int main(){
    n=read();
    for(int i=1;i<=n;++i) a[i]=read(),b[i]=read(),c[i]=read();
    dp[1][0]=a[1],dp[1][1]=b[1],dp[1][2]=c[1];
    for(int i=2;i<=n;++i){
        dp[i][0]=max(dp[i-1][1],dp[i-1][2])+a[i];
        dp[i][1]=max(dp[i-1][0],dp[i-1][2])+b[i];
        dp[i][2]=max(dp[i-1][0],dp[i-1][1])+c[i];
    }
    printf("%d\n",max({dp[n][0],dp[n][1],dp[n][2]}));
    return 0;
}

D - Knapsack 1

传统背包问题,设 \(dp_i\) 为已然使用重量 \(i\) 时的最大价值,正序枚举每个物品再倒序枚举重量即可。(完全滚掉一维,不能产生冲突)
转移方程:

\[dp_j=\max_i(dp_j,dp_{j-w_i}+v_i) \]

时间复杂度:\(O(nW)\)

点击查看代码
int n,m;
int w[maxn];
ll v[maxn];
ll dp[maxn];
ll ans;
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;++i) w[i]=read(),v[i]=read();
    for(int i=1;i<=n;++i){
        for(int j=m;j>=w[i];--j){
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    for(int i=0;i<=m;++i) ans=max(ans,dp[i]);
    printf("%lld\n",ans);
    return 0;
}

E - Knapsack 2

略微有所改动,这里的代价扩大到 \(10^9\) 级别,而总价值缩小了,因此可以仿照上一题,将状态改为 \(dp_i\) 表示得到价值为 \(i\) 的物品的最小代价,同上一题枚举即可。答案为倒序第一个最小代价合法的价值。
转移方程:

\[dp_j=\min_i(dp_j,dp_{j-v_i}+w_i) \]

时间复杂度:\(O(n\sum v)\)

点击查看代码
int n,m;
int w[105],v[105],sum;
int dp[maxn];
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;++i) w[i]=read(),v[i]=read(),sum+=v[i];
    memset(dp,0x3f,sizeof(dp));
    dp[0]=0,dp[v[1]]=w[1];
    for(int i=2;i<=n;++i){
        for(int j=sum;j>=v[i];--j){
            if(dp[j-v[i]]+w[i]>m) continue;
            dp[j]=min(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    for(int i=sum;i>=0;--i){
        if(dp[i]<=m) return printf("%d\n",i),0;
    }
    return 0;
}

F - LCS

设 \(dp_{i,j}\) 为 \(s\) 前 \(i\) 位与 \(t\) 前 \(j\) 位的最大公共子串。
转移方程:

\[dp_{i,j}=\max(dp_{i-1,j},dp_{i,j-1}) \]

当 \(s_i=t_j\) 时,还存在转移方程:

\[dp_{i,j}=\max(dp_{i,j},dp_{i-1,j-1}+1) \]

时间复杂度:\(O(|s||t|)\)
输出路径另开数组记录一下,老生常谈了。

点击查看代码
int ls,lt;
char s[3005],t[3005];
int dp[3005][3005],pre[3005][3005][2],mark[3005][3005];
void print(int x,int y){
    if(!x||!y) return;
    print(pre[x][y][0],pre[x][y][1]);
    if(mark[x][y]) printf("%c",s[x]);
}
int main(){
    scanf("%s",s+1);
    scanf("%s",t+1);
    ls=strlen(s+1),lt=strlen(t+1);
    for(int i=1;i<=ls;++i){
        for(int j=1;j<=lt;++j){
            if(s[i]==t[j]&&dp[i-1][j-1]+1>dp[i][j]){
                dp[i][j]=dp[i-1][j-1]+1;
                pre[i][j][0]=i-1,pre[i][j][1]=j-1;
                mark[i][j]=1;
            } 
            if(dp[i-1][j]>dp[i][j]){
                dp[i][j]=dp[i-1][j];
                pre[i][j][0]=i-1,pre[i][j][1]=j;
                mark[i][j]=0;
            }
            if(dp[i][j-1]>dp[i][j]){
                dp[i][j]=dp[i][j-1];
                pre[i][j][0]=i,pre[i][j][1]=j-1;
                mark[i][j]=0;
            }
        }
    }
    print(ls,lt);
    printf("\n");
    return 0;   
}

G - Longest Path

DAG 上的 dp,用拓扑排序来转移成线性问题,在队首出队并删去对节点的入度贡献时,可以同时更新答案。
转移方程:

\[dp_v=\min_{v\to u}\{dp_u\}+1 \]

时间复杂度:\(O(m)\)

点击查看代码
int n,m;
vector<int> E[maxn];
queue<int> q;
int deg[maxn];
int dp[maxn];
int ans;
int main(){
    n=read(),m=read();
    for(int i=1;i<=m;++i){
        int u=read(),v=read();
        E[u].push_back(v);
        ++deg[v];
    }
    for(int i=1;i<=n;++i){
        if(!deg[i]) q.push(i);
    }
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int v:E[u]){
            dp[v]=max(dp[v],dp[u]+1);
            --deg[v];
            if(!deg[v]) q.push(v);
        }
    }
    for(int i=1;i<=n;++i){
        ans=max(ans,dp[i]);
    }
    printf("%d\n",ans);
    return 0;
}

H - Grid 1

坐标 dp,也比较简单,设 \(dp_{i,j}\) 表示 \((1,1)\) 到 \((i,j)\) 的路径数目,对于每一个标记为 . 的坐标,从上方和左侧两个位置的状态转移即可。
转移方程:

\[dp_{i,j}=dp_{i-1,j}+dp_{i,j-1} \]

时间复杂度:\(O(HW)\)

点击查看代码
int n,m;
char s[1005][1005];
int dp[1005][1005];
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;++i) scanf("%s",s[i]+1);
    dp[1][1]=1;
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            if(i==1&&j==1) continue;
            if(s[i][j]=='.') dp[i][j]=(dp[i-1][j]+dp[i][j-1])%mod;
        }
    }
    printf("%d\n",dp[n][m]);
    return 0;
}

I - Coins

直接转移的概率 dp,设 \(dp_{i,j}\) 表示前 \(i\) 枚硬币,有 \(j\) 枚正面向上的概率。
转移方程:

\[dp_{i,j}=dp_{i-1,j-1}\times p_i+dp_{i-1,j}\times (1-p_i) \]

时间复杂度:\(O(n^2)\)

点击查看代码
int n;
db p[3005];
db dp[3005][3005];
db ans;
int main(){
    n=read();
    for(int i=1;i<=n;++i) scanf("%Lf",&p[i]);
    dp[1][0]=1.0-p[1],dp[1][1]=p[1];
    for(int i=2;i<=n;++i){
        for(int j=0;j<=i;++j){
            if(!j) dp[i][j]=dp[i-1][j]*(1.0-p[i]);
            else if(i==j) dp[i][j]=dp[i-1][j-1]*p[i];
            else dp[i][j]=dp[i-1][j-1]*p[i]+dp[i-1][j]*(1-p[i]);
        }
    }
    for(int i=(n+1)/2;i<=n;++i) ans+=dp[n][i];
    printf("%.11Lf\n",ans);
    return 0;
}

J - Sushi

带自环的期望 dp,多数要倒推+移项。
考虑 \(a\) 的值只有三种,将其列入状态中,即 \(dp(i,j,k)\) 表示还有 \(i\) 个盘子剩 \(1\) 个,\(j\) 个盘子剩 \(2\) 个,\(k\) 个盘子剩 \(3\) 个的期望步数。
转移方程:

\[dp(i,j,k)=1+\dfrac{n-(i+j+k)}{n}\times dp(i,j,k)+\dfrac{i}{n}\times dp(i-1,j,k)+\dfrac{j}{n}\times dp(i+1,j-1,k)+\dfrac{k}{n}\times dp(i,j+1,k-1) \]

化简一下得到:

\[dp(i,j,k)=\dfrac{n}{i+j+k}+\dfrac{i}{i+j+k}\times dp(i-1,j,k)+\dfrac{j}{i+j+k}\times dp(i+1,j-1,k)+\dfrac{k}{i+j+k}\times dp(i,j+1,k-1) \]

考虑到只有 \(k\) 有单调性,可以对 \(k\) 枚举,也可以记忆化搜索。
时间复杂度:\(O(n^3)\)

点击查看代码
int n;
int a[305],tot[4];
db dp[305][305][305];
db ans;
db dfs(int i,int j,int k){
    if(!i&&!j&&!k) return 0.0;
    if(dp[i][j][k]!=0) return dp[i][j][k];
    dp[i][j][k]=1.0*n/(i+j+k);
    if(i) dp[i][j][k]+=1.0*i/(i+j+k)*dfs(i-1,j,k);
    if(j) dp[i][j][k]+=1.0*j/(i+j+k)*dfs(i+1,j-1,k);
    if(k) dp[i][j][k]+=1.0*k/(i+j+k)*dfs(i,j+1,k-1);
    return dp[i][j][k];
}
int main(){
    n=read();
    for(int i=1;i<=n;++i){
        a[i]=read();
        ++tot[a[i]];
    }
    ans=dfs(tot[1],tot[2],tot[3]);
    printf("%.12LF\n",ans);
    return 0;
}

K - Stones

感觉是博弈论,但是集合大小和值域大小都可以枚举,设 \(dp_{i,0/1}\) 表示 \(i\) 个石子时,先手或后手是否必胜,这一次操作先手必胜和上一次操作的后手必胜是或运算关系。
时间复杂度:\(O(nK)\)

点击查看代码
int n,m;
int a[105];
int dp[maxn][2];
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;++i){
        a[i]=read();
        dp[a[i]][0]=1;
    }
    for(int i=1;i<=m;++i){
        for(int j=1;j<=n&&a[j]<=i;++j){
            dp[i][0]|=dp[i-a[j]][1];
        }
        dp[i][1]=dp[i][0]^1;
    }
    if(dp[m][0]) printf("First\n");
    else printf("Second\n");
    return 0;
}

L - Deque

首先要分先后手来进行 \(\max\) 或 \(\min\) 的转移,开始想了个 \(dp_{i,j}\) 表示队首取了 \(i\) 个,队尾取了 \(j\) 个的最优答案,发现假了,原因是这种正序递推无法保证上一次最优的情况就是可以实现的(先后手均采用最优决策)。
因此考虑倒推,设 \(dp_{l,r}\) 为剩余队列中 \([l,r]\) 的元素的最佳答案,这样可以讨论先后手。
已选取的个数为偶数(先手)转移方程:

\[dp_{l,r}=\max(dp_{l+1,r}+a_l,dp_{l,r-1}+a_r) \]

已选取的个数为奇数(后手)转移方程:

\[dp_{l,r}=\min(dp_{l+1,r}-a_l,dp_{l,r-1}-a_r) \]

时间复杂度:\(O(n^2)\)

点击查看代码
int n;
ll a[3005];
ll dp[3005][3005];
int main(){
    n=read();
    for(int i=1;i<=n;++i) a[i]=read();
    for(int k=1;k<=n;++k){
        for(int l=1,r=k;r<=n;++l,++r){
            if((n-k)&1) dp[l][r]=min(dp[l+1][r]-a[l],dp[l][r-1]-a[r]);
            else dp[l][r]=max(dp[l+1][r]+a[l],dp[l][r-1]+a[r]);
        }
    }
    printf("%lld\n",dp[1][n]);
    return 0;
}

M - Candies

设 \(dp_{i,j}\) 表示前 \(i\) 个位置放累计 \(j\) 个糖的方案数。
转移方程:

\[dp_{i,j}=\sum_{k=j-a_i}^j dp_{i-1,k} \]

明显的前缀和优化,需要判一下边界,再就是前缀和要算到最大,即 \(K\)。
时间复杂度:\(O(nK)\)

点击查看代码
int n,m;
int a[105],sum[105];
int dp[105][maxn],sumdp[maxn];
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;++i) a[i]=read(),sum[i]=sum[i-1]+a[i];
    for(int i=0;i<=a[1];++i) dp[1][i]=1,sumdp[i]=i+1;
    for(int i=a[1]+1;i<=m;++i) sumdp[i]=a[1]+1;
    for(int i=2;i<=n;++i){
        dp[i][0]=1;
        for(int j=1;j<=min(m,sum[i]);++j){
            if(j-a[i]>0) dp[i][j]=(sumdp[j]-sumdp[j-a[i]-1]+mod)%mod;
            else dp[i][j]=sumdp[j];
        }
        sumdp[0]=dp[i][0];
        for(int j=1;j<=m;++j){
            sumdp[j]=(sumdp[j-1]+dp[i][j])%mod;
        }
    }
    printf("%d\n",dp[n][m]);
    return 0;
}

N - Slimes

区间 dp,设 \(dp_{l,r}\) 为区间 \([l,r]\) 内的最大答案,求一个前缀和,枚举断点即可。
转移方程:

\[dp_{l,r}=\max_{i=l}^{r-1}\{dp_{l,i}+dp_{i+1,r}\}+sum(r)-sum(l-1) \]

时间复杂度:\(O(n^3)\)

点击查看代码
int n;
ll a[405],sum[405];
ll dp[405][405];
int main(){
    n=read();
    for(int i=1;i<=n;++i) a[i]=read(),sum[i]=sum[i-1]+a[i];
    memset(dp,0x3f,sizeof(dp));
    for(int i=1;i<=n;++i) dp[i][i]=0;
    for(int k=2;k<=n;++k){
        for(int l=1,r=k;r<=n;++l,++r){
            for(int i=l;i<r;++i){
                dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]+sum[r]-sum[l-1]);
            }
        }
    }
    printf("%lld\n",dp[1][n]);
    return 0;
}

O - Matching

看数据范围知状压,设 \(dp_{i,s}\) 表示前 \(i\) 个左侧点匹配右侧点状态为 \(s\) 的方案数。不难发现,转移的条件是 \(\operatorname{popcount}(s)=i\),可以优化。
时间复杂度:\(O(n2^n)\)

点击查看代码
int n;
int st[23];
int dp[23][maxn];
int stcnt[maxn];
vector<int> S[23];
int main(){
    n=read();
    for(int i=1;i<=n;++i){
        for(int j=1;j<=n;++j){
            int x=read();
            if(x) st[i]|=(1<<(j-1));
        }
    }
    for(int i=1;i<=n;++i) stcnt[1<<(i-1)]=1;
    for(int i=0;i<(1<<n);++i){
        int k=i&-i;
        if(k&&!stcnt[i]) stcnt[i]=stcnt[i^k]+1;
        S[stcnt[i]].push_back(i);
    }
    for(int i=1;i<=n;++i){
        if(st[1]&(1<<(i-1))) dp[1][(1<<(i-1))]=1;
    }
    for(int i=2;i<=n;++i){
        for(int s:S[i-1]){
            for(int j=1;j<=n;++j){
                if(s&(1<<(j-1))) continue;
                if(!(st[i]&(1<<(j-1)))) continue;
                dp[i][s|(1<<(j-1))]=(dp[i][s|(1<<(j-1))]+dp[i-1][s])%mod;
            }
        }
    }
    printf("%d\n",dp[n][(1<<n)-1]);
    return 0;
}

P - Independent Set

树形 dp,设 \(dp_{u,0/1}\) 表示 \(u\) 号节点子树内在 \(u\) 染黑或染白的情况下的方案数。注意这里求方案数而非最大答案,是乘法原理。
转移方程:

\[dp_{u,0}=\prod_{v\in G}dp_{v,1} \]

\[dp_{u,1}=\prod_{v\in G}(dp_{v,0}+dp_{v,1}) \]

时间复杂度:\(O(n)\)

点击查看代码
int n;
vector<int> E[maxn];
int dp[maxn][2];
void dfs(int u,int f){
    dp[u][0]=dp[u][1]=1;
    for(int v:E[u]){
        if(v==f) continue;
        dfs(v,u);
        dp[u][0]=1ll*dp[u][0]*dp[v][1]%mod;
        dp[u][1]=1ll*dp[u][1]*(dp[v][0]+dp[v][1])%mod;
    }
}
int main(){
    n=read();
    for(int i=1,u,v;i<n;++i){
        u=read(),v=read();
        E[u].push_back(v);
        E[v].push_back(u);
    }
    dfs(1,0);
    printf("%d\n",(dp[1][0]+dp[1][1])%mod);
    return 0;
}

Q - Flowers

朴素做法是设 \(dp_i\) 表示把 \(i\) 作为最后一个元素的最大权值和。
转移方程:

\[dp_i=\max_{1\le j<i,p_j<p_i} \{dp_j\}+a_i \]

发现是在 \(p\) 上的连续区间中取 \(\max\),可以用线段树优化。
时间复杂度:\(O(n\log n)\)

点击查看代码
int n;
int h[maxn];
ll a[maxn];
struct SegmentTree{
    #define mid ((l+r)>>1)
    #define lson rt<<1,l,mid
    #define rson rt<<1|1,mid+1,r
    ll mx[maxn<<2];
    void update(int rt,int l,int r,int p,ll k){
        if(l==r){
            mx[rt]=k;
            return;
        }
        if(p<=mid) update(lson,p,k);
        else update(rson,p,k);
        mx[rt]=max(mx[rt<<1],mx[rt<<1|1]);
    }
    ll query(int rt,int l,int r,int pl,int pr){
        if(pl<=l&&r<=pr) return mx[rt];
        ll res=0;
        if(pl<=mid) res=max(res,query(lson,pl,pr));
        if(pr>mid) res=max(res,query(rson,pl,pr));
        return res;
    }
    #undef mid
    #undef lson
    #undef rson
}S;
ll ans;
int main(){
    n=read();
    for(int i=1;i<=n;++i) h[i]=read();
    for(int i=1;i<=n;++i) a[i]=read();
    for(int i=1;i<=n;++i){
        if(h[i]==1){
            ans=max(ans,a[i]);
            S.update(1,1,n,h[i],a[i]);
        }
        else{
            ll now=S.query(1,1,n,1,h[i]-1)+a[i];
            ans=max(ans,now);
            S.update(1,1,n,h[i],now);
        }
    }
    printf("%lld\n",ans);
    return 0;
}

R - Walk

邻接矩阵可以模拟 Floyd 做法,根据矩阵乘法的结合律,使用快速幂优化。
时间复杂度:\(O(n^3\log K)\)

点击查看代码
int n;
ll m;
struct Matrix{
    int num[55][55];
    Matrix(){
        memset(num,0,sizeof(num));
    }
    Matrix operator*(const Matrix&rhs)const{
        Matrix res;
        for(int i=1;i<=n;++i){
            for(int k=1;k<=n;++k){
                for(int j=1;j<=n;++j){
                    res.num[i][j]=(res.num[i][j]+1ll*num[i][k]*rhs.num[k][j]%mod)%mod;
                }
            }
        }
        return res;
    }
}a;
inline Matrix q_pow(Matrix x,ll p){
    Matrix res=x;
    --p;
    while(p){
        if(p&1) res=res*x;
        x=x*x;
        p>>=1;
    }
    return res;
}
int ans;
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;++i){
        for(int j=1;j<=n;++j){
            a.num[i][j]=read();
        }
    }
    a=q_pow(a,m);
    for(int i=1;i<=n;++i){
        for(int j=1;j<=n;++j){
            ans=(ans+a.num[i][j])%mod;
        }
    }
    printf("%d\n",ans);
    return 0;
}

S - Digit Sum

数位 dp,使用记忆化搜索,设 \(dp_{i,0/1,j}\) 表示前 \(i\) 位有无最大值限制且加和模 \(D\) 为 \(j\) 的方案数。
需要去掉 \(0\) 的情况。
时间复杂度:\(O(D\lg K)\)

点击查看代码
int n,d;
char s[maxn];
int a[maxn];
int dp[maxn][2][105];
int dfs(int x,bool lim,int num){
    if(x==n+1){
        if(!num) return 1;
        else return 0;
    } 
    if(dp[x][lim][num]!=-1) return dp[x][lim][num];
    int res=0,mx=9;
    if(lim) mx=a[x];
    for(int i=0;i<=mx;++i){
        if(lim&&i==a[x]) res=(res+dfs(x+1,1,(num+i)%d))%mod;
        else res=(res+dfs(x+1,0,(num+i)%d))%mod;
    }
    return dp[x][lim][num]=res;
}
int ans;
int main(){
    scanf("%s",s+1);
    n=strlen(s+1);
    d=read();
    for(int i=1;i<=n;++i){
        a[i]=s[i]-'0';
    }
    memset(dp,-1,sizeof(dp));
    ans=(dfs(1,1,0)-1+mod)%mod;
    printf("%d\n",ans);
    return 0;
}

T - Permutation

排列计数 dp,发现如果将填入的数字设为状态会有不合法状态,考虑用排名作为状态。设 \(dp_{i,j}\) 表示前 \(i\) 个数中,第 \(i\) 个数从小到大排名为 \(j\) 的方案数。
对于 < 的情况,第 \(i-1\) 的排名低于 \(i\),所以转移方程:

\[dp_{i,j}=\sum_{k=1}^{j-1} dp_{i-1,k} \]

对于 > 的情况,第 \(i-1\) 的排名不低于 \(i\),注意加入 \(i\) 后 \(i-1\) 的排名会上升一位,所以转移方程:

\[dp_{i,j}=\sum_{k=j}^{i-1} dp_{i-1,k} \]

可以前缀和优化。
时间复杂度:\(O(n^2)\)

点击查看代码
int n;
char s[3005];
int dp[3005][3005];
int sum[3005];
int main(){
    n=read();
    scanf("%s",s+2);
    dp[1][1]=1;
    for(int i=1;i<=n;++i) sum[i]=1;
    for(int i=2;i<=n;++i){
        for(int j=1;j<=i;++j){
            if(j==1&&s[i]=='<') continue;
            if(j==i&&s[i]=='>') continue;
            if(s[i]=='<'){
                dp[i][j]=(dp[i][j]+sum[j-1])%mod;
            }
            else{
                dp[i][j]=(dp[i][j]+sum[i-1]-sum[j-1]+mod)%mod;
            }
        }
        for(int j=1;j<=n;++j) sum[j]=(sum[j-1]+dp[i][j])%mod;
    }
    printf("%d\n",sum[n]);
    return 0;
}

U - Grouping

先预处理出每个选取状态的贡献和 \(sum\),设 \(dp_S\) 为状态为 \(S\) 的最大贡献,枚举子集。
转移方程:

\[dp_S=\max_{S1 \subseteq S,S2=\complement_S S1} \{sum_{S1}+dp_{S2}\} \]

时间复杂度:\(O(n^22^n+3^n)\)

点击查看代码
int n;
ll a[20][20];
ll st[maxn];
ll dp[maxn];
int main(){
    n=read();
    for(int i=1;i<=n;++i){
        for(int j=1;j<=n;++j){
            a[i][j]=read();
        }
    }
    for(int s=0;s<(1<<n);++s){
        for(int i=1;i<=n;++i){
            if(!(s&(1<<(i-1)))) continue;
            for(int j=i+1;j<=n;++j){
                if(!(s&(1<<(j-1)))) continue;
                st[s]+=a[i][j];
            }
        }
    }
    for(int s=0;s<(1<<n);++s){
        for(int s1=s;s1;s1=s&(s1-1)){
            int s2=s^s1;
            dp[s]=max(dp[s],st[s1]+dp[s2]);
        }
    }
    printf("%lld\n",dp[(1<<n)-1]);
    return 0;
}

V - Subtree

换根 dp,先考虑根节点为 \(1\) 如何计算,设 \(dp_{u}\) 为该节点染成黑色的方案数。
转移方程:

\[dp_{u}=\prod_{v\in son(u)} (dp_{v}+1) \]

考虑换根,发现答案 \(ans_u\) 的来源分三部分:\(u\) 子树内的方案数,\(u\) 父亲 \(f\) 除 \(u\) 子树之外的方案数以及 \(u\) 祖先除 \(u\) 所在子树的方案数。
第一部分即为 \(dp_u\),第二部分即为:\(\dfrac{dp_f}{dp_u+1}\),第三部分实际上来自上一层第二部分。
现在问题在于模数不保证为质数,可能存在逆元,解决方法为维护前缀积和后缀积,直接算没有 \(dp_u+1\) 时 \(dp_f\) 的值。
时间复杂度:\(O(n)\)

点击查看代码
int n,mod;
vector<int> E[maxn],son[maxn],pre[maxn],suf[maxn];
int dp[maxn],ans[maxn];
void dfs(int u,int f){
    dp[u]=1;
    for(int i=0;i<E[u].size();++i){
        int v=E[u][i];
        if(v==f) continue;
        son[u].push_back(v);
        dfs(v,u);
        dp[u]=1ll*dp[u]*(dp[v]+1)%mod;
    }
    int now=1;
    for(int i=0;i<son[u].size();++i){
        int v=son[u][i];
        now=1ll*now*(dp[v]+1)%mod;
        pre[u].push_back(now);
    }
    now=1;
    for(int i=son[u].size()-1;i>=0;--i){
        int v=son[u][i];
        now=1ll*now*(dp[v]+1)%mod;
        suf[u].push_back(now);
    }
    reverse(suf[u].begin(),suf[u].end());
}
void secdfs(int u,int prod){
    ans[u]=1ll*dp[u]*(prod+1)%mod;
    for(int i=0;i<son[u].size();++i){
        int v=son[u][i],tmp=prod+1;
        if(i) tmp=1ll*tmp*pre[u][i-1]%mod;
        if(i!=son[u].size()-1) tmp=1ll*tmp*suf[u][i+1]%mod;
        secdfs(v,tmp);
    }
}
int main(){
    n=read(),mod=read();
    for(int i=1;i<n;++i){
        int u=read(),v=read();
        E[u].push_back(v);
        E[v].push_back(u);
    }
    dfs(1,0);
    secdfs(1,0);
    for(int i=1;i<=n;++i) printf("%d\n",ans[i]);
    return 0;
}

W - Intervals

据说是套路题,套路貌似在于为保证不重不漏,枚举右端点记录答案。
于是设 \(dp_{i,j}\) 表示在前 \(i\) 个位置中,上一个 \(1\) 在 \(j\) 的最大贡献,注意这里只包含右端点在 \([1,i]\) 的贡献。
转移方程:

\[dp_{i,j}=dp_{i-1,j}+\sum_{l_k\ge j,r_k=i} a_k \]

特别地,对于 \(dp_{i,i}\) 有:

\[dp_{i,i}=\max(\max_{j=1}^{i-1}\{dp_{i-1,j}\},0) \]

这样枚举状态和区间修改的复杂度都是 \(O(n^2)\) 的,使用线段树优化,可以规避掉对第二维的枚举。
时间复杂度:\(O(n\log n)\)

点击查看代码
int n,m;
struct node{
    int l,r;
    ll w;
    node()=default;
    node(int l_,int r_,ll w_):l(l_),r(r_),w(w_){}
}a[maxn];
vector<node> gr[maxn];
struct SegmentTree{
    #define mid ((l+r)>>1)
    #define lson rt<<1,l,mid
    #define rson rt<<1|1,mid+1,r
    ll mx[maxn<<2],tag[maxn<<2];
    void build(int rt,int l,int r){
        mx[rt]=0,tag[rt]=0;
        if(l==r) return;
        build(lson),build(rson);
    } 
    void push_down(int rt){
        if(tag[rt]){
            tag[rt<<1]+=tag[rt],tag[rt<<1|1]+=tag[rt];
            mx[rt<<1]+=tag[rt],mx[rt<<1|1]+=tag[rt];
            tag[rt]=0;
        }
    }
    void update(int rt,int l,int r,int pl,int pr,ll k){
        if(pl<=l&&r<=pr){
            mx[rt]+=k,tag[rt]+=k;
            return;
        }
        push_down(rt);
        if(pl<=mid) update(lson,pl,pr,k);
        if(pr>mid) update(rson,pl,pr,k);
        mx[rt]=max(mx[rt<<1],mx[rt<<1|1]);
    }
    #undef mid
    #undef lson
    #undef rson
}S;
int main(){
    n=read(),m=read();
    for(int i=1;i<=m;++i){
        int l=read(),r=read(),w=read();
        a[i]=node(l,r,w);
        gr[r].push_back(a[i]);
    }
    S.build(1,1,n);
    for(int r=1;r<=n;++r){
        S.update(1,1,n,r,r,max(S.mx[1],0ll));
        for(int j=0;j<gr[r].size();++j){
            int l=gr[r][j].l;
            ll w=gr[r][j].w;
            S.update(1,1,n,l,r,w);
        }
    }
    printf("%lld\n",max(S.mx[1],0ll));
    return 0;
}

X - Tower

比较厉害的题。
需要找到一种方式排序(不需要考虑价值),之后转换成背包问题。
貌似也是一个经典思路,考虑排序的关键字其实等价于冒泡排序中是否要交换。那么前面已经堆叠的塔重量为 \(W\),则 \(i\) 应当处于 \(j\) 之前当且仅当:

\[W+w_j\le s_i,W+w_i>s_j \]

也就是:

\[s_i+w_i<s_j+w_j \]

之后就是背包问题,只不过代价的上界是变化的。
时间复杂度:\(O(n(W+S))\)

点击查看代码
int n;
struct node{
    int w,s;
    ll v;
    bool operator<(const node&rhs)const{
        return w+s<rhs.w+rhs.s;
    }
}a[1005];
ll dp[20005];
ll ans;
int main(){
    n=read();
    for(int i=1;i<=n;++i){
        a[i].w=read(),a[i].s=read(),a[i].v=read();
    }
    sort(a+1,a+n+1);
    for(int i=1;i<=n;++i){
        for(int j=min(a[i].s+a[i].w,20000);j>=a[i].w;--j){
            dp[j]=max(dp[j],dp[j-a[i].w]+a[i].v);
        }
    }
    for(int i=0;i<=20000;++i){
        ans=max(ans,dp[i]);
    }
    printf("%lld\n",ans);
    return 0;
}

Y - Grid 2

计数 dp,不需要高难度容斥。
经典的计算路径方法,是从 \((x_1,y_1)\) 到 \((x_2,y_2)\),方案数为 \(\dbinom{x_2-x_1+y_2-y_1}{x_2-x_1}\),我们需要去掉经过障碍的方案数。
发现障碍和终点计算时的要求是一致的——之前不能经过其他障碍,于是设 \(dp_i\) 表示到第 \(i\) 个障碍且不经过其他障碍的方案数,若把终点视作第 \(n+1\) 个障碍,并将障碍递增排序,即可保证在之前可能会经过的障碍的 \(dp\) 值已然被计算。
转移方程:

\[dp_{i}=\dbinom{x_i+y_i-2}{x_i-1}-\sum_{x_j\le x_i,y_j\le y_i} dp_j\times \dbinom{x_i-x_j+y_i-y_j}{x_i-x_j} \]

时间复杂度:\(O(n^2)\)

点击查看代码
int h,w,n;
struct node{
    int x,y;
    bool operator<(const node&rhs)const{
        if(x==rhs.x) return y<rhs.y;
        return x<rhs.x;
    }
}a[3005];
int dp[3005];
int fac[maxn],finv[maxn];
inline int q_pow(int x,int p){
    int res=1;
    while(p){
        if(p&1) res=1ll*res*x%mod;
        x=1ll*x*x%mod;
        p>>=1;
    }
    return res;
}
inline int C(int N,int M){
    return 1ll*fac[N]*finv[M]%mod*finv[N-M]%mod;
}
int main(){
    h=read(),w=read(),n=read();
    fac[0]=finv[0]=1;
    for(int i=1;i<=h+w;++i) fac[i]=1ll*fac[i-1]*i%mod;
    finv[h+w]=q_pow(fac[h+w],mod-2);
    for(int i=h+w-1;i>=1;--i) finv[i]=1ll*finv[i+1]*(i+1)%mod;
    for(int i=1;i<=n;++i){
        a[i].x=read(),a[i].y=read();
    }
    sort(a+1,a+n+1);
    a[++n].x=h,a[n].y=w;
    for(int i=1;i<=n;++i){
        dp[i]=C(a[i].x+a[i].y-2,a[i].x-1);
        for(int j=1;j<i;++j){
            if(a[j].x<=a[i].x&&a[j].y<=a[i].y){
                dp[i]=(dp[i]-1ll*dp[j]*C(a[i].x+a[i].y-a[j].x-a[j].y,a[i].x-a[j].x)%mod+mod)%mod;
            }
        }
    }
    printf("%d\n",dp[n]);
    return 0;
}

Z - Frog 3

非常板的斜率优化。
设 \(dp_i\) 为到第 \(i\) 块石头的最小代价,直接来看转移方程:

\[dp_i=\min_{j=1}^{i-1}\{dp_j+(h_i-h_j)^2+C\} \]

我们把最小值去掉:

\[dp_i=dp_j+(h_i-h_j)^2+C \]

\[dp_i=dp_j+h_i^2-2\times h_i\times h_j+h_j^2+C \]

移项得到:

\[dp_i-h_i^2-C=dp_j+h_j^2-2\times h_i\times h_j \]

即得到了形如 \(b=y-kx\) 的形式。
其中:

\[\begin{cases} b=dp_i-h_i^2-C\\ y=dp_j+h_j^2\\ k=2\times h_i\\ x=h_j \end{cases}\]

求最小值,即单调队列维护一个下凸壳。
时间复杂度:\(O(n)\)

点击查看代码
int n;
ll C,h[maxn];
ll dp[maxn];
inline db X(int i){
	return 1.0*h[i];
}
inline db Y(int i){
	return 1.0*(dp[i]+h[i]*h[i]);                                                                                          
}
inline db slope(int i,int j){
	return (Y(j)-Y(i))/(X(j)-X(i));
}
int q[maxn],head,tail;
int main(){
	n=read(),C=read();
	for(int i=1;i<=n;++i) h[i]=read();
	dp[1]=0;
	head=1,tail=0;
	q[++tail]=1;
	for(int i=2;i<=n;++i){
		while(head<tail&&slope(q[head],q[head+1])<=2.0*h[i]) ++head;
		dp[i]=dp[q[head]]+(h[i]-h[q[head]])*(h[i]-h[q[head]])+C;
		while(head<tail&&slope(q[tail],i)<=slope(q[tail-1],q[tail])) --tail;
        q[++tail]=i;
	}
    printf("%lld\n",dp[n]);
	return 0;
}

标签:AtCoder,26,乱写,复杂度,int,maxn,read,main,dp
来源: https://www.cnblogs.com/SoyTony/p/16560009.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有