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


    (这是道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;
    }
    标程哈

    完结撒花!

  • 相关阅读:
    如何设置PPT,演示者能看到备注而观众看不到
    面向对象的设计原则-类设计原则
    C#路径
    MyEclipse汉化
    XML
    C#读取Excel文件
    预处理指令
    c/c++有些函数之前有export
    动态内存
    模板
  • 原文地址:https://www.cnblogs.com/XLINYIN/p/11476992.html
Copyright © 2020-2023  润新知