Hdu第八场 树形dp+基环树
每个牌背面的数字朝正面的数字连一条有向边
则题目变为问你最少翻转多少次 能使得每个数字的入度不超过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(); } }