「SOL」網路流flow (模擬賽)
題面
給定一張分層有向圖,有 \(n\) 層,每層有 \(m\) 個點。只有從第 \(i\) 層的點連向第 \(i + 1\) 層的點的連邊。
記 \(A(i,j)\) 表示從第 \(i\) 層的某些點出發到第 \(j\) 層的某些點的點不相交路徑集合的最大大小。求
\[\sum_{i=1}^n\sum_{j=i+1}^n A(i,j) \]其中,\(n\le 4\times10^4,m\le10\)。
解析
注意到 \(m\) 很小,於是首先有一個暴力狀壓的思路。\(f(i,S)\) 表示從第 \(i\) 層的 \(S\) 點集出發,要求這 \(|S|\) 條路徑都能到達第 \(j\) 層,\(j\)
可以 DFS 出所有第 \(i\) 層和第 \(i + 1\) 層之間的轉移,總複雜度 \(\mathcal{O}(2^{2m}n)\)。統計答案則可以記 \(g(i,k)\):
\[g(i,k)=\max_{|S|=k}f(i,S) \]則 \(A(i, j) = k\)(\(g(i, k) \le j \le g(i, k + 1)\)),可以快速統計答案。
考慮這樣 DP 的本質是什麼。假如把原問題建網路流,只需要把每個點拆點限制流量,就相當於求兩層之間的最大流。
而狀壓 DP 反映的是這樣一個結論:從第 \(i\) 層出發,可以在任意位置結束,儘可能流到較大的層;在這樣的前提下流最大流,則 \(A(i,j)\)
相當於一個費用流,兩層之間費用為 \(1\)。(實際上也沒有這樣去實現程式碼,只是方便理解)
考慮到這個流網路非常特殊,可以模擬流的過程。
- 由於可以在任何位置結束,所以一定是滿流的,只需要每次找到殘留網路的最長路——即能夠到達的最深的層。
- 可以直接 BFS,由於費用只和點的層數有關,所以一個點不需要多次更新,過程中維護點的訪問標記。
- 記錄增廣路的前驅,在找到層數最大的增廣路後逆序還原增廣路,更新殘留網路。
- 由於流網路分層,不可能從較大的層流向較小的層,於是將源點從第 \(i\) 層換到第 \(i + 1\) 層時,可以直接繼承殘留網路,刪去 \(i\) 和 \(i + 1\)
最後就是時間複雜度的問題了。注意到假如在一次增廣時,找到的增廣路層數為 \(p\),則最多訪問 \(pk\) 個點,每個點轉移複雜度 \(\mathcal{O}(k)\),則該次增廣複雜度為 \(\mathcal{O}(pk^2)\)。
該次增廣後,流網路的總流量增加 \(\mathcal{O}(p)\)(手動模擬一下,可以發現即使發生退流,由於我們取層數最大的增廣路,退掉的流也會補回來,該次增廣仍會使流網路總流量增加 \(p\)),而流網路總流量 \(\mathcal{O(nk)}\),所以 \(\mathcal{O}(\sum pk^2)=\mathcal{nk^3}\)。
模擬細節見程式碼。
原始碼
/* Lucky_Glass */
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
typedef long long llong;
typedef std::pair<int, int> pii;
int rin(int &r) {
int c = getchar(); r = 0;
while (c < '0' || '9' < c) c = getchar();
while ('0' <= c && c <= '9') r = r * 10 + (c ^ '0'), c = getchar();
return r;
}
const int N = 4e4 + 10, K = 10;
int n, m, nw, tot_flw;
int lnk[N << 1][K], lnk_i[N << 1][K], vis[N << 1];
int elg2[(1 << K) + 10], flw_cnt[N];
pii las_pnt[N << 1][K];
void extend(const int &sx, const int &sy) {
int ed_x = sx, ed_y = sy;
int clear_vis_pos = sx;
vis[sx] = 1 << sy;
/* 此次增廣經過的第 i 層點(防止環流?) */
std::queue<pii> que;
que.emplace(sx, sy);
while (!que.empty()) {
int ux = que.front().first, uy = que.front().second;
que.pop();
if ((ux & 1) && (ux >> 1) > (ed_x >> 1))
ed_x = ux, ed_y = uy;
/* 向下一層 */
if (ux < nw) {
if (clear_vis_pos == ux) vis[++clear_vis_pos] = 0;
int rem = lnk[ux][uy] ^ (lnk[ux][uy] & vis[ux + 1]);
while (rem) {
int vy = elg2[rem & -rem];
vis[ux + 1] |= 1 << vy;
/* las_pnt 記錄轉移點,便於還原增廣路 */
las_pnt[ux + 1][vy] = std::make_pair(ux, uy);
que.emplace(ux + 1, vy);
rem ^= rem & -rem;
}
}
/* 經過逆向邊返回上一層,lnk_i 儲存反向邊 */
if (ux > sx) {
int rem = lnk_i[ux][uy] ^ (lnk_i[ux][uy] & vis[ux - 1]);
while (rem) {
int vy = elg2[rem & -rem];
vis[ux - 1] |= 1 << vy;
las_pnt[ux - 1][vy] = std::make_pair(ux, uy);
que.emplace(ux - 1, vy);
rem ^= rem & -rem;
}
}
}
/* 更新每一層的流量,更新答案 */
for (int i = sx >> 1, lmt_i = ed_x >> 1; i <= lmt_i; ++i)
++flw_cnt[i], ++tot_flw;
/* 還原增廣路,更新殘留網路 */
while (ed_x != sx || ed_y != sy) {
// printf("(%d, %d) <- ", ed_x, ed_y);
int lasx = las_pnt[ed_x][ed_y].first,
lasy = las_pnt[ed_x][ed_y].second;
if (ed_x == lasx + 1) {
lnk[lasx][lasy] ^= 1 << ed_y;
lnk_i[ed_x][ed_y] ^= 1 << lasy;
} else {
lnk[ed_x][ed_y] ^= 1 << lasy;
lnk_i[lasx][lasy] ^= 1 << ed_y;
}
ed_x = lasx, ed_y = lasy;
}
// printf("(%d, %d)\n", sx, sy);
}
void init() {
for (int i = 0; i <= K; ++i)
elg2[1 << i] = i;
}
int main() {
freopen("flow.in", "r", stdin);
freopen("flow.out", "w", stdout);
// freopen(".\\input\\input.in", "r", stdin);
init();
rin(n), rin(m);
nw = (n << 1) - 1;
for (int i = 1; i < nw; ++i)
if (i & 1) {
static char inp[K << 1];
for (int j = 0; j < m; ++j) {
scanf("%s", inp);
for (int k = 0; k < m; ++k)
lnk[i][j] |= (inp[k] ^ '0') << k;
}
} else {
for (int j = 0; j < m; ++j)
lnk[i][j] = 1 << j;
}
llong ans = 0;
for (int i = 1; i < n; ++i) {
for (int j = 0; j < m; ++j)
extend(std::max(1, (i - 1) << 1), j);
/*
* 注意一定是從 (i - 1) << 1 開始
* 通過 ((i - 1) << 1) -> (((i - 1) << 1) - 1) 是否有邊
* 限制每個點只能用一次
* 因為第一層不存在訪問多次,可以直接從 1 開始跑
*/
ans += (tot_flw -= flw_cnt[i - 1]);
}
printf("%lld\n", ans);
return 0;
}
THE END
Thanks for reading!
揚起遠航的帆
誰在輕聲呼喚
縈繞在耳畔
到達心的彼岸
一切宛如夢幻
又歸於平淡
——《巫山雲》By Snapmod / 詩岸
> Link 巫山雲 - 網易雲