[WorldWide_D幻想乡♂模拟赛][JZOJ4599]西行妖

Preface

今天射电磷( P5++5e )的模拟赛把大家都虐了~
最后一题个人认为质量很吼,就在这里记录一下。


题目大意

一棵 1 为根的树n个节点,你最多可以选择 S 叶子节点,然后将它们到根节点路径染黑。求有多少种方案能染黑至少m个节点。
1mn1000,1S20


题目分析

Algorithm 1

最暴力的 dp 就是设 fi,j,k 表示以 i 为根,选择了j个叶子,染黑了 k 个节点的方案数,转移子树合并乱搞一下即可。
然后Samjia直接使用启发式合并优化了这个算法,合并子树时使用类似合并果子的算法,用一个堆维护合并代价最小的两个块合并,时间复杂度应该是可以证明的。

Algorithm 2

以下是WorldWide_D标解。
fi,j,k表示统计到第 i 个叶子节点,已经选择了j个叶子,染黑了 k 个节点的方案数。
转移方程显然:

fi,j,k=fi,j1,k(high(i)high(lca(i,i)))

时间复杂度 O(n3s)
比赛时我打了这个并且在实现时使用 dfs ,然而如果注意到 dfs 序的性质,可以将其优化。
DFN(i)<DFN(i) ,显然 high(lca(i,i)) 随着 DFN(i) 的增大而不上升。
设当前节点为 i ,我们令
sj,k,d=lca(i,i)=dfi,j,k

再令

cntj,kd=sj,k,d

显然对于一个叶子节点 fi,j,k=cntj1,khigh(i) ,然后更新 sj,k,high(i) 以及 cnt
然后当退出一棵子树时,我们的 i 对原本子树中的点取lca深度肯定会加 1 ,因此令原本子树根节点深度为d,我们要将 sj,k,d 的值加到 sj,k,d1 里面,然后清空 sj,k,d ,同时更新 cnt 数组。
具体细节比较多,可能会有些抽象,希望读者自行思考。
时间复杂度 O(n2s)


代码实现

#include <iostream>
#include <cstdio>
#include <cctype>
#include <cmath>

using namespace std;

int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (!isdigit(ch)) f=ch=='-'?-1:f,ch=getchar();
    while (isdigit(ch)) x=x*10+ch-'0',ch=getchar();
    return x*f;
}

const int P=1000000007;
const int N=1005;
const int E=N<<1;
const int S=22;

int fa[N],high[N],last[N],next[E],tov[E],d[N],p[N],pos[N];
int f[N][S][N],cnt[S][N],sum[S][N][N];
bool vis[N];
int n,m,s,tot,l,ans;

void insert(int x,int y){tov[++tot]=y,next[tot]=last[x],last[x]=tot;}

void dfs(int x)
{
    for (int i=last[x],y;i;i=next[i])
        high[y=tov[i]]=high[x]+1,d[x]++,dfs(y);
}

void dp(int x)
{
    if (!d[x])
    {
        p[++l]=x;
        f[l][1][high[x]]=1;
        for (int j=1;j<=s;j++)
            for (int k=1;k<=n;k++)
                if (k-high[x]>=0)
                    (f[l][j][k]+=cnt[j-1][k-high[x]])%=P;
        for (int j=1;j<=s;j++)
            for (int k=1;k<=n;k++)
            {
                if (!f[l][j][k]) continue;
                (sum[j][k][high[x]]+=f[l][j][k])%=P;
                if (k-high[x]>=0) (cnt[j][k-high[x]]+=f[l][j][k])%=P;
            }
    }
    else for (int i=last[x],y;i;i=next[i]) dp(y=tov[i]);
    if (x==1) return;
    for (int j=1;j<=s;j++)
        for (int k=1;k<=n;k++)
        {
            if (!sum[j][k][high[x]]) continue;
            if (k-high[x]>=0) (((cnt[j][k-high[x]]-=sum[j][k][high[x]])%=P)+=P)%=P;
            (sum[j][k][high[x]-1]+=sum[j][k][high[x]])%=P;
            if (k-high[x]+1>=0) (cnt[j][k-high[x]+1]+=sum[j][k][high[x]])%=P;
            sum[j][k][high[x]]=0;
        }
}

int main()
{
    freopen("tree.in","r",stdin),freopen("tree.out","w",stdout);
    n=read(),m=read(),s=read();
    for (int i=2;i<=n;i++) fa[i]=read(),insert(fa[i],i);
    high[1]=1,dfs(1);
    l=0,dp(1),ans=0;
    for (int i=1;i<=l;i++)
        for (int j=1;j<=s;j++)
            for (int k=m;k<=n;k++)
                (ans+=f[i][j][k])%=P;
    printf("%d\n",ans);
    fclose(stdin),fclose(stdout);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值