1. 程式人生 > >Hdu第八場 樹形dp+基環樹

Hdu第八場 樹形dp+基環樹

個數字 樹形dp src int n) 一次 == += ret

Card Game

每個牌背面的數字朝正面的數字連一條有向邊

則題目變為問你最少翻轉多少次 能使得每個數字的入度不超過1

首先判斷圖中每個連通塊是不是樹或者基環樹 因為只有樹或者基環樹能使得每個點的入度不超過1

判的話就直接判斷邊的數量與點的數量之間的大小關系如果邊數<=點數則可行

對於樹 我們進行兩次dp 第一次dp出以一個點為根需要翻轉的次數 第二次就可以轉移出以每個點為根需要翻轉的次數

對於基環樹 我們先看環裏面需要翻轉的次數 再判斷環之外需要翻轉的次數 環之外的方向是確定的 很好判斷

技術分享圖片
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;i++) #define per(i,a,n) for (int i=n-1;i>=a;i--) #define pb push_back #define mp make_pair #define all(x) (x).begin(),(x).end() #define fi first #define se second #define SZ(x) ((int)(x).size()) typedef vector<int> VI; typedef long long ll; typedef pair
<int, int> PII; const ll mod = 998244353; ll powmod(ll a, ll b) { ll res = 1; a %= mod; assert(b >= 0); for (; b; b >>= 1) { if (b & 1) { res = res * a % mod; } a
= a * a % mod; } return res; } ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; } // head const int N = 401000; int n, m, u, v, T, mt[N], hs[N], _; vector<PII> e[N]; VI vec[N]; int vis[N], f[N], se[N], q[N], dp[N], pre[N], deg[N], fa[N]; PII chke[N]; int find(int u) { return f[u] == u ? u : f[u] = find(f[u]); } void solve() { n = 0; T++; rep(i, 1, m + 1) { scanf("%d%d", &u, &v); if (mt[u] != T) //離散化 { mt[u] = T, hs[u] = n++; } if (mt[v] != T) { mt[v] = T, hs[v] = n++; } u = hs[u]; v = hs[v]; chke[i] = mp(u, v); //記錄每條邊 } rep(i, 0, n) //初始化 e[i].clear(), vis[i] = 0, f[i] = i, vec[i].clear(), se[i] = 0; rep(i, 1, m + 1) { u = chke[i].fi, v = chke[i].se; f[find(u)] = find(v); //一個連通塊的點屬於一個father e[u].pb(mp(v, i)); //建邊 i為正表示該邊是反向與原來相反 e[v].pb(mp(u, -i)); } rep(i, 0, n) //把一個連通塊的點放到father的vector中 vec[find(i)].pb(i); rep(i, 1, m + 1) { u = chke[i].fi, v = chke[i].se; se[find(u)]++; //統計每個連通塊中邊的數量 } int ans = 0, ans2 = 1; rep(i, 0, n) { if (find(i) != i) //每個連通塊只會枚舉一次 { continue; } if (se[i] > SZ(vec[i])) //如果邊數大於點數 則不是基環樹也不是樹 答案不存在 { puts("-1 -1"); return; } if (se[i] < SZ(vec[i])) { //如果是樹的話 dp兩次 第一次dp出以0為根的翻轉次數 第二次dp出全部點的翻轉次數 int s = 1e9, s2 = 0; dp[i] = 0; int t = 1; q[0] = i; fa[i] = -1; rep(j, 0, t) { u = q[j]; for (auto p : e[u]) { int v = p.fi; if (v != fa[u]) { fa[q[t++] = v] = u, pre[v] = p.se > 0, dp[i] += pre[v]; } } } rep(j, 1, t) { u = q[j]; if (pre[u] == 1) { dp[u] = dp[fa[u]] - 1; } else { dp[u] = dp[fa[u]] + 1; } } rep(j, 0, t) s = min(s, dp[q[j]]); //當前樹所需要的最少翻轉次數 rep(j, 0, t) if (dp[q[j]] == s) { s2++; //統計有多少個方案數 } ans = ans + s; ans2 = (ll)ans2 * s2 % mod; } if (se[i] == SZ(vec[i])) //基環樹 { int s = 0; for (auto u : vec[i]) { deg[u] = SZ(e[u]); //統計每一個點的出度 } int t = 0; for (auto u : vec[i]) if (deg[u] == 1) { q[t++] = u; } rep(j, 0, t) { u = q[j]; for (auto p : e[u]) { int v = p.fi; if (deg[v] <= 1) { continue; } if (p.se < 0) { s++; } --deg[v]; if (deg[v] == 1) { q[t++] = v; } } } int rt = -1; for (auto u : vec[i]) if (deg[u] == 2) { rt = u; break; } int pree = 1e9, cnt = 0, u = rt, s3 = 0; while (1) { for (auto p : e[u]) if (deg[p.fi] == 2 && p.se + pree != 0) { s3 += p.se < 0; cnt++; pree = p.se; u = p.fi; break; } if (u == rt) { break; } } s3 = min(s3, cnt - s3); //環中的翻轉最少次數是確定的 s += s3; //環外的方向是確定的 環外需要翻轉的次數加上環中翻轉的次數 ans += s; if (s3 == cnt - s3) { ans2 = ans2 * 2 % mod; } } } printf("%d %d\n", ans, ans2); } int main() { for (scanf("%d", &_); _; _--) { scanf("%d", &m); solve(); } }
//Card Game

Hdu第八場 樹形dp+基環樹