SZUACM week4-3 题目及题解

文章目录

  • 没有上司的舞会(树形dp模板题)
  • P1122最大子树和 (树形dp)
  • P2396 yyy loves Maths VII (状压dp)
  • 洛谷P3694 邦邦的大合唱站队(状态压缩)
  • P4999 烦人的数学作业 (数位dp)

没有上司的舞会(树形dp模板题)

题目链接
状态设计:
我们先只考虑没有下属的员工,因为他们的快乐值和别人没关系,我们可以直接确定如果他快乐值大于0就来,否则就不来。
考虑有下属的,并且其下属无下属的员工,如果来了,可能会惹下属不快乐,但也可能他的快乐值很高。那怎么办呢?我们需要进行权衡,“他来并且他的直接下属不来” 与 “他不来,下属员工爱来不来”这两种情况的快乐值进行比较。
d p [ u ] [ 0 ] dp[u][0] dp[u][0]表示以u为根的子树 u u u不来的最大快乐值, d p [ u ] [ 1 ] dp[u][1] dp[u][1]表示 u u u来时以u为根的子树中来的最大快乐值。
状态转移: u u u来时,他的下属一定不能来,此时 d p [ u ] [ 1 ] dp[u][1] dp[u][1]只能从 d p [ v ] [ 1 ] dp[v][1] dp[v][1]转移来( v v v表示 u u u的下属),不必进行选择。当 u u u不来时,他的下属可来可不来,因此从 m a x ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] ) max(dp[v][0],dp[v][1]) max(dp[v][0],dp[v][1])转移过来。

#include
using namespace std;
const int N=6e3+10;
//简单建边
struct E{
    int nxt,to;
}e[N];
int head[N],cnt;
int dp[N][2],a[N],rt,n;
bool vis[N];
void add(int u,int v){
    e[++cnt]={head[u],v};
    head[u]=cnt;
}
void dfs(int u,int fa){
    dp[u][0]=0,dp[u][1]=a[u];
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa)continue;
        dfs(v,u);
        dp[u][0]+=max(dp[v][0],dp[v][1]);
        dp[u][1]+=dp[v][0];
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<n;i++){
        int l,k;cin>>l>>k;
        add(k,l);
        vis[l]=true;
    }
    for(int i=1;i<=n;i++){
        if(!vis[i]){
            rt=i;
            break;
        }
    }
    dfs(rt,0);
    cout<<max(dp[rt][0],dp[rt][1]);
    return 0;
}

P1122最大子树和 (树形dp)

题目链接
状态设计:
这题和上道题很类似,先随便选取一个点作为树根, d p [ u ] dp[u] dp[u]表示以u为根的子树的最大美丽指数。
状态转移: 每个节点的子节点可选可不选,选择的标准很简单,只要子节点的美丽指数和大于0,那就不把该子节点修建掉,将其指数和加到当前的节点,每次确定一个节点的最大指数和之后,用它更新最后的答案。

#include
using namespace std;
const int N=16000+7;
struct E{
    int nxt,to;
}e[N<<2];
int head[N],cnt;
int dp[N],a[N],n;
int ans=-2147483647;
bool vis[N];
void add(int u,int v){
    e[++cnt]={head[u],v};
    head[u]=cnt;
}
void dfs(int u,int fa){
    dp[u]=a[u];
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa)continue;
        dfs(v,u);
        if(dp[v]>0)dp[u]+=dp[v];
    }
    ans=max(ans,dp[u]);
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int i=1;i<n;i++){
        int a,b;scanf("%d%d",&a,&b);
        add(a,b),add(b,a);
        vis[b]=true;
    }
    dfs(1,0);
    printf("%d",ans);
    return 0;
}

P2396 yyy loves Maths VII (状压dp)

题目链接
这是很模板的状压题,有一个小优化使我转T为A。
状态设计: d p [ i ] dp[i] dp[i]表示手里牌状态为 i i i时的方案数。状态 i i i的含义是,如果 i i i的二进制第 j j j位是1,那么第 j j j张牌还在yyy手里,否则已经扔出去,yyy也走了对应步数。那么初始状态就是 d p [ ( 1 < < n ) − 1 ] dp[(1<dp[(1<<n)1],表示所有牌还在yyy手里,目标状态就是 d p [ 0 ] dp[0] dp[0],表示yyy手里一张牌也没有剩。
状态转移: 使用刷表法,即当前的状态为 i i i,如果这个状态是合法的,那就向下一个状态 n x t nxt nxt更新, n x t nxt nxt i i i这个状态再去掉一张牌,只要枚举一下 i i i这个状态的二进制位中为 1 1 1的位置,将其减去就可以获得 i i i对应的所有 n x t nxt nxt。但是我们其实不用枚举从第1位到第 n n n位,利用 l o w b i t lowbit lowbit求出 i i i的最后一个1所在的位置,获得第一个 n x t nxt nxt状态,然后将此位置的 1 1 1删去,继续求下一个1的位置。

#include
using namespace std;
const int N=1<<24,mod=1e9+7;
int n,m;
struct Node{
    int cnt,pos;
}dp[N];
int a[N],bad[2];
inline bool check(int pos){
    for(int i=0;i<m;i++){
        if(pos==bad[i])return false;
    }return true;
}
inline int lb(int x){return x&(-x);}
int main(){
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[1<<i];
    cin>>m;
    for(int i=0;i<m;i++)cin>>bad[i];
    dp[(1<<n)-1].cnt=1;
    for(int i=(1<<n)-1;i>=0 ;i--){
        int cur=i;
        if(!dp[i].cnt)continue;
        while(cur){
            int tmp=lb(cur),nxt=i-tmp;
            dp[nxt].pos=dp[i].pos+a[tmp];
            if(check(dp[nxt].pos))
                dp[nxt].cnt=(dp[nxt].cnt+dp[i].cnt)%mod;
            cur-=tmp;
        }
    }
    cout<<dp[0].cnt<<endl;
    return 0;
}

洛谷P3694 邦邦的大合唱站队(状态压缩)

题目链接
状态设计: 团队数量很少仅为20,考虑使用状态压缩,设 d p [ s ] dp[s] dp[s]为状态为 s s s的队形需要出队的最少人数。状态 s s s的含义是:若 s s s j j j个位置为1,表示第 j j j个团队的人都已经靠在一起。
预处理状态:
s u m [ i ] [ j ] sum[i][j] sum[i][j]:前 i i i个人中属于团队 j j j的人数;
l e n [ s ] len[s] len[s]:状态 s s s中已经排列好的人数;
n u m [ j ] num[j] num[j]:团队 j j j的总人数。
状态转移: d p [ s ] dp[s] dp[s]转移向 d p [ s 1 ] dp[s1] dp[s1]:排完 s s s中的团队(状态s不包含第 j j j个队伍), s 1 = s    ^ ( 1 < < j ) s1=s\hat{\space\space}(1<s1=s  ^(1<<j),表示接下来将第 j j j个团队,排列在 s s s状态中已经排列好的团队之后。具体过程如下图:
SZUACM week4-3 题目及题解_第1张图片
其实最朴素的想法就是对团队编号进行全排列,然后计算不同排列需要出队的人数取最小值;但是全排列肯定TLE,我们把排列拆分为一层层看,第1层确定第1个位置分配给哪个团队,第2层是在确定了第1个位置分配给哪个团队后再枚举第2个位置分配给的团队…给排列那棵树多了很多剪枝,假设第 1 , 3 , 4 1,3,4 1,3,4个团队排在前面,已经知道排列 143 143 143可以使出队人数最少,那我们每次用它们继续扩展下一层时,就少了 134 , 314 , 341 , 413 , 431 134,314,341,413,431 134,314,341,413,431这几种排列的拓展。从而将原本 O ( k ! ) O(k!) O(k!)的复杂度降低为 O ( 2 k ) O(2^k) O(2k)

#include
#define LL long long
using namespace std;
const int N=1e5+7,M=1<<20;
int dp[M],n,m,sum[N][20],num[20],len[M];
int main(){
    cin>>n>>m;
    memset(dp,0x3f,sizeof dp);
    
    for(int i=1;i<=n;i++){
        int x;cin>>x;
        x--;num[x]++;
        for(int j=0;j<m;j++){
            sum[i][j]=sum[i-1][j];
        }sum[i][x]++;
    }
    
    //预处理每个状态i已经排好的总人数
    for(int i=0;i< 1<<m;i++){
        for(int j=0;j<m;j++){
            if(i&(1<<j))len[i]+=num[j];
        }
    }
    
    //起始状态是一个团队都没有排列 
    dp[0]=0;
    
    for(int s=0;s< 1<<m;s++){
        for(int j=0;j<m;j++){
            if(s&(1<<j))continue;
            int s1=s^(1<<j);
            dp[s1]=min(dp[s1],dp[s]+num[j]-(sum[len[s1]][j]-sum[len[s]][j]));
        }
    }
    cout<<dp[(1<<m)-1]<<endl;
    return 0;
}

P4999 烦人的数学作业 (数位dp)

题目链接

题目大意: 求区间 l — r l—r lr的数字和。但是数字和定义为每个数字不同数位的和,如123=1+2+3=6。

解题思路: l , r l,r l,r的范围是 1 0 18 10^{18} 1018。暴力枚举复杂度是 O ( r − l ) O(r-l) O(rl),显然不行。我们换一个方向枚举:枚举数码1-9在 l l l r r r出现的总次数,再分别和当前枚举的数码相乘就是所求答案。比如区间 97 − 102 : 97 , 98 , 99 , 100 , 101 , 102 97-102:97,98,99,100,101,102 97102:97,98,99,100,101,102:1出现3次,9出现4次,2,7,8均只出现1次。答案为 1 × 3 + 9 × 4 + 2 + 7 + 8 1×3+9×4+2+7+8 1×3+9×4+2+7+8。同时利用前缀和思想,先计算 0 0 0 r r r之间的数字和,再计算 0 0 0 l − 1 l-1 l1之间的数字和,再用前者减去后者即为 l l l r r r之间的数字和。

数位dp一般是求区间 l l l r r r之间符合限制条件的数字个数。 l l l r r r都是达到 long long 级别的数,无法直接枚举求答案;要求数字的限制条件一般是针对数字的数位的(数位指一个数中每一个数字所占的位置,这些位置,都叫做数位。从右端算起,第一位是“个位”,第二位是“十位”…数位总和为位数,如1234为四位数),所以解决这种问题,不会一个数字一个数字看,而是枚举数位后捕获一大把合法的数字。

状态设计: 最低位标记为第1位,当前枚举数码为 d i g i t digit digit d p [ i ] [ j ] dp[i][j] dp[i][j]表示从最高位到i位时 d i g i t digit digit的个数为 j j j时总的个数为多少。因为是从高位到低位枚举的,因此枚举完最后一个位置时,相当于枚举了一个完整的数字;而刚枚举最高位置时,此状态包含了所有数字。因此我们的答案就是枚举最高位置时的状态对应的数字和。

举个例子(假设我们只考虑当前数位最多有4位,能用的数字只有1和2,以此来简化问题)。
假设现在统计的 d i g i t digit digit是1,我们枚举了前两位为 12 12 12,此时计算的就是 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1],表示从最高位到第3位时1(当前 d i g i t digit digit)的个数为1时总的个数。那么后两位有4种情况可以选择: 11 , 12 , 21 , 22 11,12,21,22 11,12,21,22
第一种情况 11 11 11:相当于枚举了第一个合法的数字: 1211 1211 1211 d i g i t 1 digit 1 digit1 共3个。
第二种情况 12 12 12:相当于枚举了第二个合法的数字: 1212 1212 1212 d i g i t 1 digit 1 digit1 共2个。
第三种情况 21 21 21:相当于枚举了第三个合法的数字: 1221 1221 1221 d i g i t 1 digit 1 digit1 共2个。
第四种情况 22 22 22:相当于枚举了第四个合法的数字: 1222 1222 1222 d i g i t 1 digit 1 digit1 共1个。
因此 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1]可以算出为:3+2+2+1=8。

接下来我们枚举前两位为 21 21 21,此时计算的还是 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1],仍然表示从最高位到第2位时1的个数为1时的总的个数。而这个答案之前已经计算过了,没有继续往低位枚举的必要,因为后面4种情况枚举之后如下:
第一种情况 11 11 11:相当于枚举了第一个合法的数字: 2111 2111 2111 d i g i t 1 digit 1 digit1 共3个。
第二种情况 12 12 12:相当于枚举了第二个合法的数字: 2112 2112 2112 d i g i t 1 digit 1 digit1 共2个。
第三种情况 21 21 21:相当于枚举了第三个合法的数字: 2121 2121 2121 d i g i t 1 digit 1 digit1 共2个。
第四种情况 22 22 22:相当于枚举了第四个合法的数字: 2122 2122 2122 d i g i t 1 digit 1 digit1 共1个。
因此 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1]仍然为:3+2+2+1=8。

最高位没有数字,第2位为1,此时仍然计算 d p [ 2 ] [ 1 ] dp[2][1] dp[2][1],仍然是8 ( 111 , 112 , 121 , 122 ) (111,112,121,122) (111,112,121,122)

当从最高位到第 i i i位时 d i g i t digit digit个数为 j j j,如果 d p [ i ] [ j dp[i][j dp[i][j已经计算过了,就不必再次计算,直接使用之前保存过的值就好,这就是记忆化搜索( ̄y▽, ̄)╭ 。

但是如果最大的数字为2112,那么枚举前两位是21的时候接下去那一位只能是1(如果是2就超过最大数了),因此之前计算好的 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1]不能使用,因为后面无法枚举完整的4种情况,只能老老实实往下算。在程序中用1个 b o o l bool bool变量 l i m i t limit limit来判断这种情况,如果枚举到当前数位 l i m i t limit limit为真,则不能使用已经保存的相应 d p dp dp值;若当前 l i m i t limit limit为真,且当前数位枚举的数字恰好和最大值对应数位的数字一致,则继续向更低位枚举时的 l i m i t limit limit也为真。但其实有 l i m i t limit limit限制的状态规模并不像有 l i m i t limit limit限制的那么多,这就像一些零零碎碎的状态需要另外处理,只要有限制,就既不能使用之前计算好的 d p dp dp值,这个状态自食其力地更新完也不能给 d p dp dp值用。

代码展示:

#include
#define LL long long
const int mod=1e9+7;
using namespace std;
int num[20],digit;
LL dp[20][20];//1ong long的类型最多有18位
LL dfs(int pos,bool limit,int sum){ 
    LL ans=0;
    if(pos==0)return sum;
    if(!limit&&dp[pos][sum]!=-1)return dp[pos][sum];
    int up=limit?num[pos]:9;//根据最大的数字确定上界
    for(int i=0;i<=up;i++){
        ans=(ans+dfs(pos-1,limit&&(up==i),sum+(i==digit)))%mod;
    }
    if(!limit)return dp[pos][sum]=ans;
    return ans;
}
LL solve(LL cur){
    num[0]=0;//num[0]表示cur的位数
    LL res=0;
    while(cur){
        num[++num[0]]=cur%10;
        cur/=10;
    }
    for(int i=1;i<=9;i++){
        memset(dp,-1,sizeof dp);
        digit=i;
        res=(res+dfs(num[0],1,0)*i%mod)%mod;
    }
}
int main(){
    int T;cin>>T;
    while(T--){
        LL l,r;cin>>l>>r;
        cout<<(solve(r)-solve(l-1)+mod)%mod<<endl;//取模之后可能r比l-1的数字和更小,加上mod保证是正数
    }
}

但是其实可以多开一个维度来记录 l i m i t limit limit为0或为1,减少上面那种每次是否可以更新 d p dp dp值的简单判断。

#include
#define LL long long
const int mod=1e9+7;
using namespace std;
int num[20],digit;
LL dp[20][20][2];//1ong long的类型最多有18位
LL dfs(int pos,bool limit,int sum){
    LL ans=0;
    if(pos==0)return sum;
    if(dp[pos][sum][limit]!=-1)return dp[pos][sum][limit];
    int up=limit?num[pos]:9;//根据最大的数字确定上界
    for(int i=0;i<=up;i++){
        ans=(ans+dfs(pos-1,limit&&(up==i),sum+(i==digit)))%mod;
    }
    return dp[pos][sum][limit]=ans;
}
LL solve(LL cur){
    num[0]=0;//num[0]表示cur的位数
    LL res=0;
    while(cur){
        num[++num[0]]=cur%10;
        cur/=10;
    }
    for(int i=1;i<=9;i++){
        memset(dp,-1,sizeof dp);
        digit=i;
        res=(res+dfs(num[0],1,0)*i%mod)%mod;
    }
}
int main(){
    int T;cin>>T;
    while(T--){
        LL l,r;cin>>l>>r;
        cout<<(solve(r)-solve(l-1)+mod)%mod<<endl;//取模之后可能r比l-1的数字和更小,加上mod保证是正数
    }
}

你可能感兴趣的:(算法,动态规划,深度优先)