[codeforces 917D]Stranger Trees

题目大意

给定一棵n个节点的树。对于k=0到n-1,输出有多少棵n个节点的带标号无根树恰好与给定的树有k条公共边。答案模 109+7
n≤100

分析

我们从prufer序的角度考虑这道题。
选择了若干条原树的边后,我们得到一些联通块。把这些联通块都缩在一起,假设剩下m个点,接下来我们要去连接这些联通块,得到一棵树,然后考虑这棵树的prufer序是怎样的。
由于当前叶子的父亲所表示的联通块G中任意一个点都可能与叶子联通块相连,所以有 |G| 种可能。那么prufer序的m-2位共有 (mi=1|Gi|)m2=nm2 种可能情况。
但是prufer序不能完全表示这棵树,因为由prufer序只能确定父亲联通块中哪个点连了出去,不知道连到哪个点,假设叶子联通块是G’,那么还有 |G| 种可能情况。再算上连接最后两个联通块的边的可能情况,所以上式还要乘上 mi=1|Gi| 才能正确算出剩下m个联通块的答案。
知道上面的东西就可以dp了。设f[i][j][k]表示原树中以i为根的子树,分成了j个联通块(可以由此知道选了多少条原树的边),其中i所在联通块大小为k时的答案。转移时枚举儿子,然后枚举儿子的后两维。转移看起来有五重循环,但是可以通过树形背包的分析方法发现时间复杂度其实是 O(n4)
这样就做完了吗?没有!
我们发现这样算出来的是与原树至少有k条公共边的答案,需要一个 O(n2) 的容斥算出正确答案。这个就不具体写了。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N=105,mo=1e9+7;

typedef long long LL;

int n,f[N][N][N],Size[N],g[N][N],ans,C[N][N],F[N],tot,h[N],e[N<<1],nxt[N<<1];

void Add(int x,int y)
{
    e[++tot]=y; nxt[tot]=h[x]; h[x]=tot;
}

void dp(int x,int y)
{
    Size[x]=1;
    f[x][1][1]=1;
    for (int i=h[x];i;i=nxt[i]) if (e[i]!=y)
    {
        dp(e[i],x);
        memset(g,0,sizeof(g));
        for (int j=1;j<=Size[x];j++)
        {
            for (int p=1;p<=Size[x]-j+1;p++) if (f[x][j][p])
            {
                for (int k=1;k<=Size[e[i]];k++)
                {
                    for (int q=1;q<=Size[e[i]]-k+1;q++) if (f[e[i]][k][q])
                    {
                        g[j+k][p]=(g[j+k][p]+(LL)f[x][j][p]*f[e[i]][k][q]%mo*q)%mo;
                        g[j+k-1][p+q]=(g[j+k-1][p+q]+(LL)f[x][j][p]*f[e[i]][k][q])%mo;
                    }
                }
            }
        }
        Size[x]+=Size[e[i]];
        memcpy(f[x],g,sizeof(g));
    }
}

int main()
{
    scanf("%d",&n);
    for (int i=1,x,y;i<n;i++)
    {
        scanf("%d%d",&x,&y);
        Add(x,y); Add(y,x);
    }
    dp(1,0);
    C[0][0]=1;
    for (int i=1;i<=n;i++)
    {
        C[i][0]=1;
        for (int j=1;j<=i;j++) C[i][j]=(C[i-1][j-1]+C[i-1][j])%mo;
    }
    F[n-1]=f[1][1][n];
    for (int i=2;i<=n;i++)
    {
        F[n-i]=0;
        for (int j=1;j<=n-i+1;j++) F[n-i]=(F[n-i]+(LL)f[1][i][j]*j)%mo;
        for (int j=0;j<i-2;j++) F[n-i]=(LL)F[n-i]*n%mo;
    }
    for (int i=0;i<n;i++)
    {
        for (int j=i+1,sig=-1;j<n;j++,sig=-sig) F[i]=(F[i]+(LL)sig*F[j]*C[j][i])%mo;
        if (F[i]<0) F[i]+=mo;
        printf("%d ",F[i]);
    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CodeForces - 616D是一个关于找到一个序列中最长的第k好子段的起始位置和结束位置的问题。给定一个长度为n的序列和一个整数k,需要找到一个子段,该子段中不超过k个不同的数字。题目要求输出这个序列最长的第k好子段的起始位置和终止位置。 解决这个问题的方法有两种。第一种方法是使用尺取算法,通过维护一个滑动窗口来记录\[l,r\]中不同数的个数。每次如果这个数小于k,就将r向右移动一位;如果已经大于k,则将l向右移动一位,直到个数不大于k。每次更新完r之后,判断r-l+1是否比已有答案更优来更新答案。这种方法的时间复杂度为O(n)。 第二种方法是使用枚举r和双指针的方法。通过维护一个最小的l,满足\[l,r\]最多只有k种数。使用一个map来判断数的种类。遍历序列,如果当前数字在map中不存在,则将种类数sum加一;如果sum大于k,则将l向右移动一位,直到sum不大于k。每次更新完r之后,判断i-l+1是否大于等于y-x+1来更新答案。这种方法的时间复杂度为O(n)。 以上是两种解决CodeForces - 616D问题的方法。具体的代码实现可以参考引用\[1\]和引用\[2\]中的代码。 #### 引用[.reference_title] - *1* [CodeForces 616 D. Longest k-Good Segment(尺取)](https://blog.csdn.net/V5ZSQ/article/details/50750827)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Codeforces616 D. Longest k-Good Segment(双指针+map)](https://blog.csdn.net/weixin_44178736/article/details/114328999)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值