【2020 牛客多校】第十場 Identical Trees 【樹形DP 最大權匹配】
阿新 • • 發佈:2020-08-14
題意
給出兩顆同構樹:
每次可以修改一個節點值,問最少需要修改多少次,使得兩棵樹一樣。
錯誤思路
比賽的時候直接把兩棵樹的所有根節點到葉子節點的鏈提取出來,當做一個二分圖,長度相同的鏈,左邊樹的鏈向右邊連邊,權值為節點編號不同的個數,然後跑最大權匹配。
沒怎麼寫過最大權,找 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; }