【2020 牛客多校】第十场 Identical Trees 【树形DP 最大权匹配】

Identical Trees

题意

给出两颗同构树:

image-20200814084654628

每次可以修改一个节点值,问最少需要修改多少次,使得两棵树一样。

错误思路

比赛的时候直接把两棵树的所有根节点到叶子节点的链提取出来,当做一个二分图,长度相同的链,左边树的链向右边连边,权值为节点编号不同的个数,然后跑最大权匹配。

没怎么写过最大权,找 bug 找到比赛结束。找完成功WA了。

如果要连边,那么要考虑到子树同构的问题,如果不是同构是不能连边的

题解

树形 DP + 二分图最大权匹配

思路就是让第一棵树的链匹配第二颗树的链,编号尽可能的相同。

同时 dfs 两棵树

匹配之前要检查子树是否同构。

如果匹配,直接暴力枚举两个子树的儿子节点,接着进行 dfs 。

获得任意两个儿子匹配之后的边权,跑最大权匹配。

代码

/*
 * @Autor: valk
 * @Date: 2020-07-17 16:50:40
 * @LastEditTime: 2020-08-14 09:08:18
 * @Description: 如果邪恶 是 华丽残酷的乐章 它的终场 我会亲手写上 晨曦的光 风干最后一行忧伤 黑色的墨 染上安详
 */
#include <bits/stdc++.h>
#define fuck system("pause")
#define emplace_back push_back
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const ll mod = 1e9 + 7;
const ll seed = 12289;
const double eps = 1e-6;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const ll N = 1000 + 10;

//KM 模板
ll lx[N], ly[N]; //同时调节两个数组,使得权值和最大
ll n;
//n1,n2为二分图的顶点集,其中x属于n1,y属于n2
//link记录n2中的点y在n1中所匹配的x点的编号
ll link[N];
ll slack[N]; //松弛操作
ll visx[N], visy[N];
bool dfs(vector<vector<ll>>& w, ll x)
{
    visx[x] = 1; //得到发生矛盾的居民集合
    //对于这个居民,每个房子都试一下,找到就退出
    for (ll y = 1; y <= n; y++) {
        if (visy[y])
            continue; //不需要重复访问
        ll t = lx[x] + ly[y] - w[x][y]; //这个条件下使用匈牙利算法
        if (t == 0) //标志这个房子可以给这个居民
        {
            visy[y] = 1;
            //这个房子没人住或者可以让住着个房子的人去找另外的房子住
            if (link[y] == 0 || dfs(w,link[y])) {
                link[y] = x;
                return 1; //可以让这位居民住进来
            }
        } else if (slack[y] > t) //否则这个房子不能给这位居民
            slack[y] = t;
    }
    return 0;
}
ll KM(vector<vector<ll>>& w)
{
    memset(lx, 0, sizeof(lx));
    memset(ly, 0, sizeof(ly));
    memset(link, 0, sizeof(link));
    //首先把每个居民出的钱最多的那个房子给他
    for (ll i = 1; i <= n; i++)
        for (ll j = 1; j <= n; j++)
            if (lx[i] < w[i][j])
                lx[i] = w[i][j];

    //在满足上述条件之后,给第i位居民分配房子
    for (ll i = 1; i <= n; i++) {
        for (ll j = 1; j <= n; j++)
            slack[j] = inf; //松弛量
        while (1) //直到给这个居民找到房子为止
        {
            memset(visx, 0, sizeof(visx));
            memset(visy, 0, sizeof(visy));
            if (dfs(w,i))
                break; //找到房子,就跳出循环
            ll d = inf;
            for (ll k = 1; k <= n; k++)
                if (!visy[k] && d > slack[k])
                    d = slack[k]; //找到最小松弛量
            for (ll k = 1; k <= n; k++) //松弛操作,使发生矛盾的居民有更多选择
            {
                if (visx[k])
                    lx[k] -= d;
                //将矛盾居民的要求降低,使发生矛盾的居民有更多

                if (visy[k])
                    ly[k] += d;
                //使发生矛盾的房子在下一个子图,保持矛盾
            }
        }
    }
    ll ans = 0;
    for (ll i = 1; i <= n; i++)
        ans += w[link[i]][i];
    return ans;
}

vector<ll> v1[N], v2[N];
ll dp[N][N];//dp[i][j]表示左树以 i 为根节点的子树和右树以 j 为根节点的子树匹配需要修改的最小次数

ll dfs1(ll x, ll y)
{
    if (dp[x][y] >= 0)
        return dp[x][y];
    if (v1[x].size() != v2[y].size())//不是同构
        return dp[x][y] = (1LL << 30);
    dp[x][y] = (x != y);//当前节点编号不一样,则需要修改
    
    vector<vector<ll>> w(v1[x].size() + 1, vector<ll>(v2[y].size() + 1));//KM 的邻接矩阵

    for (ll i = 0; i < v1[x].size(); i++) {
        for (ll j = 0; j < v2[y].size(); j++) {//暴力匹配
            dp[v1[x][i]][v2[y][j]] = dfs1(v1[x][i], v2[y][j]);
            w[i + 1][j + 1] = -dp[v1[x][i]][v2[y][j]]; //建边,因为要求最小所以边权取负
        }
    }
    n = v1[x].size();// KM 的点数
    dp[x][y] -= KM(w);// 加上子树匹配需要修改的最小次数
    return dp[x][y] = min(dp[x][y], (1LL << 30));
}

int main()
{
    memset(dp, -1, sizeof(dp));
    ll m;
    scanf("%lld", &m);
    for (ll i = 1; i <= m; i++) {
        ll x;
        scanf("%lld", &x);
        v1[x].pb(i);
    }
    for (ll i = 1; i <= m; i++) {
        ll x;
        scanf("%lld", &x);
        v2[x].pb(i);
    }
    printf("%lld\n", dfs1(0, 0));
    return 0;
}
posted @ 2020-08-14 09:34  Valk3  阅读(145)  评论(0编辑  收藏  举报