E - Minimize Sum of Distances
https://atcoder.jp/contests/abc348/tasks/abc348_e
換根DP or 帶權樹的重心
換根DP
如果只求根節點的 \(f_x\),那就是一個很簡單的樹形DP(甚至沒用dp吧,就dfs一遍):
- \(f(x) = \displaystyle \sum_{i = 1}^N(C_i\times d(x, i))\)
std::vector<i64> f(N);//即f(i) = sum(C(i) * d(x, i))
std::vector<int> d(N);//d[i]為d(0, i)的路徑長度
std::vector<i64> sum_c(N);//預處理出包括當前節點的對應子樹的所有C的和
auto pre_dfs = [&](auto self, int now, int fa) {//預處理出從點0出發的f(0) = sum(C(i) * d(0, i))
sum_c[now] = C[now];
for (const auto& to : adj[now]) if (to != fa) {
d[to] = d[now] + 1; dfs(dfs, now, to); sum_c[now] += sum_c[to];
}
};
pre_dfs(pre_dfs, 0, -1); for (int i = 0; i < N; i++) {f[0] += 1LL * C[i] * d[i];}
但是要求出所有節點的 \(f(x)\) 來找最小值。對求出來的根節點的 \(f(0)\) 進行換根即可:
- 考慮對 \(0\) 的子節點 \(1\) (這裡 \(1\) 是 \(0\) 的左子樹)換根:
- 顯然對於 \(1\) 及其子樹,所有度數都少 \(1\),再各自乘以 \(C_i\) 後也就是減去一倍的 \(sumC_1\)
- 然後要加上 \(0\) 及其右子樹,所有度數都多一,再各自乘以 \(C_i\) 後也就是加上一倍的 \(sumC_0 - sumC_1\),即除了 \(C_1\) 的子樹之外的所有子樹的 \(C\).
auto dfs = [&](auto self, int now, int fa)->void {
for (const auto& to : adj[now]) if (to != fa) {
f[to] = f[now] - sum_c[to] + (sum_c[0] - sum_c[to]); self(self, to, now);
}
};
dfs(dfs, 0, -1); std::cout << ranges::min(f) << '\n';
樹的重心
-
定義
-
如果在樹中選擇某個節點並刪除,這棵樹將分為若干棵子樹,統計子樹節點數並記錄最大值。取遍樹上所有節點,使此最大值取到最小的節點被稱為整個樹的重心。
-
(這裡以及下文中的「子樹」若無特殊說明都是指無根樹的子樹,即包括「向上」的那棵子樹,並且不包括整棵樹自身。)
-
-
性質
-
樹的重心如果不唯一,則至多有兩個,且這兩個重心相鄰。
-
以樹的重心為根時,所有子樹的大小都不超過整棵樹大小的一半。
-
樹中所有點到某個點的距離和中,到重心的距離和是最小的;如果有兩個重心,那麼到它們的距離和一樣。
-
把兩棵樹透過一條邊相連得到一棵新的樹,那麼新的樹的重心在連線原來兩棵樹的重心的路徑上。
-
在一棵樹上新增或刪除一個葉子,那麼它的重心最多隻移動一條邊的距離。
-
-
求法
- 在 DFS 中計算每個子樹的大小,記錄「向下」的子樹的最大大小,利用總點數 - 當前子樹(這裡的子樹指有根樹的子樹)的大小得到「向上」的子樹的大小,然後就可以依據定義找到重心了。
那麼這題如果 \(\forall C_i = 1\) 就是板題,不過去掉這個限制也依然有一個很直接的想法:
- 我們由樹的重心的定義:以樹的重心為根時,所有子樹的大小都不超過整棵樹大小的一半。
- 感性推導一下:帶權樹的重心,就是所有子樹的大小都不超過 \(\displaystyle \frac{1}{2} \sum_{i=1}^{N} C_i\)
然後用求重心的思路求就可以了。
signed main()
{
std::cin.tie(nullptr)->sync_with_stdio(false);
int N; std::cin >> N;
std::vector adj(N, std::vector<int>()); for (const auto _ : views::iota(0, N - 1)) {int A, B; std::cin >> A >> B; --A; --B; adj[A].push_back(B); adj[B].push_back(A);}
std::vector<int> C(N); for (auto& x : C) {std::cin >> x;}
std::vector<int> d(N);//d[i]為d(0, i)的路徑長度
std::vector<i64> sum_c(N);//預處理出包括當前節點的對應子樹的所有C的和
auto pre_dfs = [&](auto self, int now, int fa)->void {//預處理出每個點及其子樹的sum_c
sum_c[now] = C[now];
for (const auto& to : adj[now]) if (to != fa) {
self(self, to, now); sum_c[now] += sum_c[to];
}
};
pre_dfs(pre_dfs, 0, -1);
const i64 tot_c{std::accumulate(C.begin(), C.end(), 0LL)};
auto find_centroid = [&](auto self, int now, int fa)->int {//找重心
bool is_centroid{true};
for (const auto& to : adj[now]) if (to != fa) {
const int res{self(self, to, now)};
if (res != -1) {return res;}//如果子樹已經找到重心了,那就直接返回
if (sum_c[to] > tot_c / 2) {is_centroid = false;}//向下子樹不能比tot/2大
}
if ((tot_c - sum_c[now]) > tot_c / 2) {is_centroid = false;}//向上子樹不能比tot/2大
return is_centroid ? now : -1;
};
const int centroid{find_centroid(find_centroid, 0, -1)};
auto calc_d = [&](auto self, int now, int fa) -> void {//找到重心後,以重心為起點計算d
for (const auto& to : adj[now]) if (to != fa) {
d[to] = d[now] + 1; self(self, to, now);
}
};
calc_d(calc_d, centroid, -1);
i64 ans{}; for (const auto i : views::iota(0, N)) {ans += 1LL * d[i] * C[i];}
std::cout << ans << '\n';
return 0;
}
F - Oddly Similar
https://atcoder.jp/contests/abc348/tasks/abc348_f
bitset 卡常題
顯然的最暴力的寫法,複雜度為 \(O(N^2M) = O(8\times10^9)\)。
就差一點能過。
不久前用到的一個思路,先改變列舉的順序:
第一重列舉還是列舉行 \(i\),但是第二重直接列舉該行的列的元素,然後對每個元素列舉在其他行有沒有出現,再統計相等次數為奇數的。
先寫出這個思路:
signed main()
{
std::cin.tie(nullptr)->sync_with_stdio(false);
int N, M; std::cin >> N >> M;
std::vector A(N, std::vector<int>(M)); for (auto& vec : A) for (auto& x : vec) {std::cin >> x;}
std::vector f(M, std::vector(K, std::vector<bool>(KN)));//f[column位置j][具體數值A][出現在i行] = 1 表示 i 行,j 列 有出現 A
for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) {f[j][A[i][j]][i] = true;}
int ans{};
for (int i = 0; i < N; i++) {
std::vector<bool> g(N);//即 0 ~ N,每一行相同的數字出現次數是否為奇數(是的話或完就是1)
for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) {g[i] ^= f[j][A[i][j]][i];}
g[i] = 0;//自己那個答案肯定是0
ans += ranges::count(g, true);//所有結果為1的行,都是出現了奇數次
}
std::cout << ans / 2 << '\n';//他要求 i < j 的對數
return 0;
}
仍然跑了一樣的複雜度。
但是我們發現 \(g_i, f_{j,A_{i.j}}\) 後都是表示 \(i\in [0, n)\) 的行數的狀態,大小都相同,所以可以用 std::bitset
代替,這樣就有最佳化了 \(\frac{1}{64}\)
時間複雜度大概是 \(O(1\times 10 ^ 8)\) 能過
signed main()
{
std::cin.tie(nullptr)->sync_with_stdio(false);
int N, M; std::cin >> N >> M;
std::vector A(N, std::vector<int>(M)); for (auto& vec : A) for (auto& x : vec) {std::cin >> x;}
std::vector f(M, std::vector(K, std::bitset<KN>()));//f[column位置j][具體數值A][出現在i行] = 1 表示 i 行,j 列 有出現 A
for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) {f[j][A[i][j]].set(i);}
int ans{};
for (int i = 0; i < N; i++) {
std::bitset<KN> g{};//即 0 ~ N,每一行相同的數字出現次數是否為奇數(是的話或完就是1)
for (int j = 0; j < M; j++) {g ^= f[j][A[i][j]];}
g.reset(i);//自己那個答案肯定是0
ans += g.count();//所有結果為1的行,都是出現了奇數次
}
std::cout << ans / 2 << '\n';//他要求 i < j 的對數
return 0;
}