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

习题11 Dyslexic Gollum UVA - 1633 (状态压缩)

题目链接
参考博客
因为要求 01 01 01串中不包含长度至少为 k k k的回文串,只要不包含长度为 k k k的回文串,就不包含长度大于 k + 2 k+2 k+2的回文串(相当于在两边补上同样的字符),因此只要确保回文串长度无法达到 k k k k + 1 k+1 k+1即可。
状态设计: d p [ i ] [ j ] dp[i][j] dp[i][j]为前 i i i个位置已经安排好,安排好的 i i i个中后 k + 1 k+1 k+1个字符二进制数字为 j j j时,满足条件的字符串数量。每次我们向后扩展一个位置:前 i − 1 i-1 i1个位置安排好了,第 i i i个位置放下 0 0 0 1 1 1之后,从 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]转移过来,同时要求加入 0 0 0 1 1 1都不能产生长度达到 k k k k + 1 k+1 k+1的回文串。因为前面的都是满足要求的,即回文串长度不会超过 k k k,假设第 i i i位再放置 0 0 0,可能产生非法的情况只有两种:

  1. 回文串长度直接达到 k + 1 k+1 k+1,即当前 i − 1 i-1 i1个位置安排好时,状态为 j j j的后 k − 1 k-1 k1个字符构成回文串,而倒数第 k k k个字符恰好为 0 0 0,此时会构成"0+长度为 ( k − 1 ) (k-1) (k1)的回文串+0"的长度为 k + 1 k+1 k+1的字符串。
  2. 回文串长度达到 k k k,即当前 i − 1 i-1 i1个位置安排好时,状态为 j j j的后 k − 2 k-2 k2个字符构成回文串,而倒数第 k − 1 k-1 k1个字符恰好为 0 0 0,此时会构成"0+长度为 ( k − 2 ) (k-2) (k2)的回文串+0"的长度为 k k k的字符串。

排除掉这两种情况(若枚举第 i i i位为 1 1 1也同理需要排除掉这两种情况),其他情况都直接从 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]转移过来。
状态转移: 其实不算状态转移,而称为“刷表法”,就是用当前的状态去更新相关的状态。枚举前 i − 1 i-1 i1个的所有状态,然后将所有合法状态相加(即除了以上列出的两种非法情况)。

#include<bits/stdc++.h>
using namespace std;
const int N=410,K=1<<11,mod=1e9+7;
int dp[N][K];
int n,k;
bool check(int str,int len){
    for(int i=0,j=len-1;i<j;i++,j--){
        if((1&(str>>i))!=(1&(str>>j)))
            return false;
    }return true;
}
int main(){
    int T;cin>>T;
    while(T--){
        cin>>n>>k;
        if(k>n){
            cout<<(1<<n)<<endl;continue;
        }
        if(k==1){
            cout<<0<<endl;continue;
        }
        memset(dp,0,sizeof dp);
        dp[0][0]=1;
        for(int i=1;i<=n;i++){
            for(int j=0;j< (1<<k+1);j++){
                if(dp[i-1][j]){
                    bool judge1=1,judge0=1;
                    if(i>k&&check(j&((1<<k-1)-1),k-1)){
                        if(j&(1<<k-1))judge1=0;
                        else judge0=0;
                    }
                    if(i>=k&&check(j&((1<<k-2)-1),k-2)){
                        if(j&(1<<k-2))judge1=0;
                        else judge0=0;
                    }
                    int cur=(j&(1<<k)-1)<<1;
                    if(judge0)dp[i][cur]=(dp[i][cur]+dp[i-1][j])%mod;
                    cur++;
                    if(judge1)dp[i][cur]=(dp[i][cur]+dp[i-1][j])%mod;
                }
            }
        }
        int ans=0;
        for(int i=0;i<( 1<<k+1);i++)
            ans=(ans+dp[n][i])%mod;
        cout<<ans<<endl;
    }
}

或者可以像以下用 s t [ s ] st[s] st[s]数组存储每个状态 s s s是否合法,即不是回文串,然后再向后一位转移。

#include<bits/stdc++.h>
using namespace std;
const int N=410,K=1<<11,mod=1e9+7;
bool st[K];
int dp[N][K];
int n,k;
inline bool check(int str,int len){
    for(int i=0,j=len-1;i<j;i++,j--)
        if((1&(str>>i))!=(1&(str>>j)))
            return true;
    return false;
}
int main(){
    int T;cin>>T;
    while(T--){
        cin>>n>>k;
        if(k>n){cout<<(1<<n)<<endl;continue;}
        if(k==1){cout<<0<<endl;continue;}
        if(k==n){
            int ans=0;
            for(int i=0;i<1<<k;i++)
                if(check(i,k))ans++;
            cout<<ans<<endl;continue;
        }

        k++;
        for(int i=0;i<1<<k;i++)
            st[i]=check(i>>1,k-1)&check(i^(1<<k-1),k-1)&check(i,k);

        memset(dp,0,sizeof dp);

        for(int i=0;i< 1<<k;i++)
            if(st[i])dp[k][i]=1;

        for(int i=k+1;i<=n;i++){
            for(int j=0;j< 1<<k;j++){
                if(st[j]){
                    int t=((j&((1<<k-1)-1))<<1);
                    if(st[t])dp[i][t]=(dp[i][t]+dp[i-1][j])%mod;
                    t++;
                    if(st[t])dp[i][t]=(dp[i][t]+dp[i-1][j])%mod;
                }
            }
        }

        int ans=0;
        for(int i=0;i< 1<<k;i++)
            if(st[i])ans=(ans+dp[n][i])%mod;
        cout<<ans<<endl;
    }
}

习题12 Protecting Zonk UVA - 12093 (树形dp)

题目链接
参考题解
树形dp第一步:选定一个根,将无根树转化为有根树,之后设定 d p [ i ] [ j ] dp[i][j] dp[i][j]表示当前以 i i i为根的子树,且该子树状态为 j j j的最优解。
最难的就是对状态的设计,使得状态的转移可以覆盖所有情况。

这道题最简单的状态是3个:设当前节点为u,则u可以配置装置A;u配置装置B;u不配置任何装置。但是因为装置B的功能不只局限在父子中间,子节点B的可以扩展到祖父和父亲,父节点B可以扩展到儿子和孙子。因此应该细化状态,因为某条边被覆盖可能是被父亲,祖父,儿子和孙子。因为我们已经把无根树转化为有根树,只考虑当前节点u到孩子的连边的覆盖者,并不是我们不考虑节点u到父亲的连边,因为节点u到父亲的连边是在父亲那一层进行考虑的。
状态设计:
当u有配置A或B,u到孩子连边必然会被覆盖;否则该连边不是被u的子节点覆盖,就是被u的父节点覆盖。因此设计出以下状态:

  1. dp[u][0]:u无装置,u到孩子的连边被孩子覆盖
  2. dp[u][1]:u有装置A
  3. dp[u][2]:u有装置B
  4. dp[u][3]:u无装置,u到孩子的连边被父亲覆盖

状态转移:
设v为u的子节点们。

  1. d p [ u ] [ 0 ] = ∑ m i n ( d p ( v , 1 , 2 ) dp[u][0]=\sum{min(dp(v,1,2)} dp[u][0]=min(dp(v,1,2)
    u被子节点覆盖,每个孩子应该配备A或B,因此无法从0和3转移来。
  2. ( a ) : d p [ u ] [ 1 ] = ∑ m i n ( d p ( v , 0 , 1 , 2 ) + c 1 (a):dp[u][1]=\sum{min(dp(v,0,1,2)}+c1 (a):dp[u][1]=min(dp(v,0,1,2)+c1
    因为dp[u][1]表示u有装置A,而A只能覆盖u的相邻边,无法覆盖孩子和孙子的连边,因此无法从dp[v][3]转移来。
    u配备装置A可以使得u的相邻边被覆盖,还有什么情况也可以使得u的相邻边被覆盖呢?就是u的某个孩子配置B!我们需要进行比较,即是”u配备A“花费更低还是“u的孩子配备B”花费更低。后者可以表示为:
    ( b ) : ∑ m i n ( d p ( v , 0 , 1 , 2 ) ) + m i n ( d p [ k ] [ 2 ] − m i n ( d p ( k , 0 , 1 , 2 ) ) (b):\sum{min(dp(v,0,1,2))} +min(dp[k][2]-min(dp(k,0,1,2)) (b):min(dp(v,0,1,2))+min(dp[k][2]min(dp(k,0,1,2))
    其中v表示u的所有子节点,k表示那个配备装置B的子节点,因为它已经配备了B,就排除其他原本可能取到的最小状态。
    最后比较 d p [ u ] [ 1 ] dp[u][1] dp[u][1]在(a)(b)两式子中取小值。
  3. d p [ u ] [ 2 ] = ∑ m i n ( d p ( v , 0 , 1 , 2 , 3 ) + c 2 dp[u][2]=\sum{min(dp(v,0,1,2,3)}+c2 dp[u][2]=min(dp(v,0,1,2,3)+c2 有B就是王者,可以从子节点的每个状态转移而来。
  4. d p [ u ] [ 3 ] = ∑ m i n ( d p ( v , 0 , 1 , 2 ) dp[u][3]=\sum{min(dp(v,0,1,2)} dp[u][3]=min(dp(v,0,1,2)因为u与孩子节点被父亲覆盖,孩子有无配备A或B均可,但无法转移到3。
#include<bits/stdc++.h>
#define min3(a,b,c) min(a,min(b,c))
const int inf=0x3f3f3f3f;
using namespace std;
const int N=1e4+10;
vector<int>son[N];
int n,c1,c2;
int dp[N][4];
void dfs(int u,int fa){
    dp[u][0]=dp[u][3]=0,dp[u][1]=0,dp[u][2]=c2;
    int minB=inf;
    for(int i=0;i<son[u].size();i++){
        int v=son[u][i];
        if(v==fa)continue;
        dfs(v,u);
        int minn=min3(dp[v][0],dp[v][1],dp[v][2]);
        dp[u][0]+=min(dp[v][1],dp[v][2]);
        dp[u][2]+=min(minn,dp[v][3]);
        dp[u][3]+=minn;
        minB=min(minB,dp[v][2]-minn);
    }
    dp[u][1]=dp[u][3]+min(c1,minB);
}
int main(){
    while(cin>>n>>c1>>c2&&n){
        for(int i=0;i<=n;i++)son[i].clear();
        memset(dp,0x3f,sizeof dp);
        for(int i=1;i<n;i++){
            int x,y;cin>>x>>y;
            son[x].push_back(y),son[y].push_back(x);
        }
        dfs(1,0);
        cout<<min3(dp[1][0],dp[1][1],dp[1][2])<<endl;
    }
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值