1. 程式人生 > 實用技巧 >【2020 牛客多校】第十場 Identical Trees 【樹形DP 最大權匹配】

【2020 牛客多校】第十場 Identical Trees 【樹形DP 最大權匹配】

Identical Trees

題意

給出兩顆同構樹:

每次可以修改一個節點值,問最少需要修改多少次,使得兩棵樹一樣。

錯誤思路

比賽的時候直接把兩棵樹的所有根節點到葉子節點的鏈提取出來,當做一個二分圖,長度相同的鏈,左邊樹的鏈向右邊連邊,權值為節點編號不同的個數,然後跑最大權匹配。

沒怎麼寫過最大權,找 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;
}