神奇脑洞题解——树的最大匹配

(这是道CEOI2007的原题,洛谷上也有哦)

COGS  489

至于为啥没有洛谷链接,实验人怎么能用别人的评测机

其实只是洛谷数据过强,要写高精度的

一句话题面:给定一颗父子关系指明的树,记树上某个点和他的父亲可以形成一对匹配,求这棵树最多可以形成多少匹配,形成这么多种匹配的方案有几种?

【输入格式】

第一行一个数 N ,表示有多少个结点。

接下来 N 行,每行第一个数,表示要描述的那个结点的编号。然后一个数 m ,表示这个结点有 m 个儿子,接下来 m 个数,表示它的 m 个儿子的编号。

【输出格式】

输出两行,第一行为最大匹配数,第二行输出最大匹配方案数。

【输入样例】

7
1 3 2 4 7
2 1 3
4 1 6
3 0
7 1 5
5 0
6 0

【输出样例】

3
4

【数据规模】

N<=1000, 其中 40% 的数据答案不超过 10^7 

看数据就知道要写高精,但COGS可以用long long int 水过去哈

首先考虑第一个问题:最大匹配是多少?

这一看就知道是个树形DP,而且每个点上的取值和这个点的状态有关

按照树形DP的套路,状态可以设为在x的子树内,能匹配的最大匹配对数(因为每个点只能和自己的父亲与儿子匹配,因此,各个子树间互不影响)

先大概脑补一下没有上司的舞会这道题,如果没写过的话直接肝这题有点难度哈

按照没有上司的舞会的套路,每个点状态分两种:dp[x][0]和dp[x][1]分别代表当点x不与自己儿子匹配时x内子树匹配最大值和当x参与时的最大值。

那么现在问题就明朗许多了。

先大概思考一下,对于dp[x][0]的计算,因为x点是不用的,因此x的儿子y是否被使用都无所谓,也就是说dp[x][0]=∑max(dp[y][0],dp[y][1]);

这里还可以再加强一波:可以得知dp[y][1]>=dp[y][0];

这个东西比较神奇,但是也可以感性理解一下。假设我们现在有一棵小树,这棵树有两个儿子,两个儿子的子树各是一条链,(实际上这个时候模拟的就是树的最底部)链有多长咱们不管,但是至少我们可以知道,匹配肯定是先选一条边两端的两个节点,但是与其相连的两条边都不能选,贪心可知,叶子节点一定要选,然后这样的话,假设一条长度为3的链,选上树根就可以多得到一对,偶数长度的选上树根也不行,所以说dp[y][1]>=dp[y][0]当且仅当y的子树的链长度均为偶数时取等号。

所以说dp[x][0]=∑dp[y][1];

考虑完dp[x][0],再考虑一下dp[x][1],众所周知,因为x是与自己的某个儿子匹配的,因此可以认为dp[x][1]可以认为是由∑dp[y][1]然后减去min(dp[y][1]-dp[y][0])再+1得到的

这里可以将求dp[x][0]和求dp[x][1]合在一起(因为dp[x][0]就是dp[y][1]之和)

提一句,维护顺序是先维护dp[x][1]再维护dp[x][0]这样可以有效利用之前计算的信息

下面的代码中,dp[x][0]是代表前i-1个子树dp[y][1]之和,而dp[x][1]已经开始维护第i个子树的影响了。

if(dp[x][1])
{ dp[x][
1]+=dp[to][1]; } if(dp[x][0]+dp[to][0]+1>dp[x][1]) { dp[x][1]=dp[x][0]+dp[to][0]+1; }//因为开始时dp[x][1]初始值是0,那么直接进入下一步,否则先将当前y不和x相连的贡献算上,然后再考虑是否能更换那个与x相连的儿子(相当于假定第一个儿子与x相连,然后依次尝试替换)

 好了,第一步,维护最大值完成了,下一问,计算方案数!

这道题说实话难就难到计算方案数上了。

仿照上面设计状态

g[x][0]代表,以x为根的子树,在不选x的情况下,达到最大值的方案数。

g[x][1]则代表选x的情况下的方案数。

我们先考虑g[x][0]的做法,

子树y对g[x][0]的贡献无非也就以下几类

  1. 当dp[y][0]==dp[y][1]时(上面有介绍这种情况)
  2. 当dp[y][0]!=dp[y][1]时

相信各位大佬的数学还是不错的,知道方案数计算应该用乘法吧,不知道的出门右拐小学奥数班哈

对于第一种情况,g[x][0]=g[x][0]*(g[y][0]+g[y][1])选哪个都一样,那我都要,小孩子才做选择。

第二种我木的选择 g[x][0]=g[x][0]*g[y][1]只能这样选了。

OK,对g[x][0]的维护完成了,下面开始考虑g[x][1]

首先声明维护顺序:一定要先维护dp[x][1]和g[x][1]

if(dp[x][1])
        {
            dp[x][1]+=dp[to][1];
            if(dp[to][0]!=dp[to][1])
            {
                g[x][1]*=g[to][1];
            }
            else
            {
                g[x][1]*=g[to][0]+g[to][1];
            }
        }
        if(dp[x][0]+dp[to][0]+1>dp[x][1])
        {
            dp[x][1]=dp[x][0]+dp[to][0]+1;
            g[x][1]=g[x][0]*g[to][0];
        }
        else
        if(dp[x][0]+dp[to][0]+1==dp[x][1])
            g[x][1]+=g[x][0]*g[to][0];

大家仔细看看,这份代码和上面是一个框架,但是加入了对g[x][1]的维护(比如最下面的else)

维护大概分以下几个步骤

  1. 先看看是不是取dp[y][0]和dp[y][1]对dp[x][1]的贡献一样(这个时候那个和自己父亲链接的点已经有了,现在的y先假定不和父亲链接)
  2. 如果现在的点取代之前的点和x相连更加合适,那么就更换,这时候就体现出先维护dp[x][1]的优点了,此时的g[x][0]是y之前所有子树取dp[子树][1]的选法(可能有dp[子树][0]==dp[子树][1]),那么这个时候注意是直接赋值,将g[x][1]=g[x][0]*g[y][0];
  3. 如果当前的点和之前选的那个一样优,这就相当于又贡献了一堆选法,注意这个时候是加法。
  4. 完了

这样的话,这道题就算完事了。

#include<iostream>
#include<cstdio>
#include<vector>
#define int long long int
using namespace std;
int dp[1001][2];
int g[1001][2];
int n,m,a1,root;
bool rd[1001];
vector<int> b[1001];
void DFS(int x,int fa)
{
    dp[x][0]=0;
    dp[x][1]=0;
    g[x][0]=1;
    g[x][1]=1;
    int mxf=0,sum=0;
    for(int i=0;i<b[x].size();i++)
    {
        int to=b[x][i];
        DFS(to,x);
        //dp[x][1]=max(dp[x][1],dp[to][0]-max(dp[to][0],dp[to][1])+1+dp[x][0]);
        if(dp[x][1])
        {
            dp[x][1]+=dp[to][1];
            if(dp[to][0]!=dp[to][1])
            {
                g[x][1]*=g[to][1];
            }
            else
            {
                g[x][1]*=g[to][0]+g[to][1];
            }
        }
        if(dp[x][0]+dp[to][0]+1>dp[x][1])
        {
            dp[x][1]=dp[x][0]+dp[to][0]+1;
            g[x][1]=g[x][0]*g[to][0];
        }
        else
        if(dp[x][0]+dp[to][0]+1==dp[x][1])
            g[x][1]+=g[x][0]*g[to][0];
        dp[x][0]+=dp[to][1];
        //dp[x][0]=max(dp[x][0],dp[x][0]+max(dp[to][0],dp[to][1]));
         if(dp[to][0]!=dp[to][1]) 
             g[x][0]*=g[to][1];
        else
             g[x][0]*=g[to][0]+g[to][1];
    }
    if(!dp[x][1])
        g[x][1]=0;
} 
signed main()
{
    freopen("treeb.in","r",stdin);
    freopen("treeb.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int ke;
        scanf("%d",&ke);
        scanf("%d",&m);
        for(int j=1;j<=m;j++)
        {
            scanf("%d",&a1);
            rd[a1]++;
            b[ke].push_back(a1);
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(rd[i]==0)
        {
            root=i;
            break;
        }
    }
    DFS(root,0);
    cout<<dp[root][1]<<endl;
    if(dp[root][0]!=dp[root][1])
    {
        cout<<g[root][1];
    }
    else
    cout<<g[root][1]+g[root][0];
    return 0;
}
标程哈

完结撒花!

原文地址:https://www.cnblogs.com/XLINYIN/p/11476992.html