比賽傳送門:CF #1137。
比賽記錄:點我。
每次都自閉的 div1 啊,什么時候才能上 IM 呢。
【A】Skyscrapers
題意簡述:
有一個 \(n\times m\) 的矩陣 \(a_{ij}\)。
對於每個 \((i,j)\)(\(1\le i\le n\),\(1\le j\le m\)),你把第 \(i\) 行和第 \(j\) 列單獨抽出,這樣就有 \(n+m-1\) 個數被你抽出。
你可以對這些數重新標號為正整數,但是要滿足第 \(i\) 行所有數的大小關系不變,第 \(j\) 列所有數的大小關系不變(兩個限制相互獨立)。
滿足這個限制下,你希望最大的標號盡量小,對每個 \((i,j)\) 求出這個最小的最大標號。
題解:
因為行大小關系不變,列大小關系不變,我們考慮分別離散化每行每列,並統計每個數在行內和列內的排名。
若 \((i,j)\) 在行內排名為第 \(x\) 小,列內排名為第 \(y\) 小,它最優情況下就被標號為 \(\max(x,y)\),然后行內、列內比它大的數依次往大排。
由此寫出代碼:
#include <cstdio>
#include <algorithm>
const int MN = 1005;
int N, M, A[MN][MN];
int X[MN][MN], Y[MN][MN];
int B[MN], C1[MN], C2[MN], C;
int main() {
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= M; ++j)
scanf("%d", &A[i][j]);
for (int i = 1; i <= N; ++i) {
for (int j = 1; j <= M; ++j) B[j] = A[i][j];
std::sort(B + 1, B + M + 1), C1[i] = C = std::unique(B + 1, B + M + 1) - B - 1;
for (int j = 1; j <= M; ++j) X[i][j] = std::lower_bound(B + 1, B + C + 1, A[i][j]) - B;
}
for (int j = 1; j <= M; ++j) {
for (int i = 1; i <= N; ++i) B[i] = A[i][j];
std::sort(B + 1, B + N + 1), C2[j] = C = std::unique(B + 1, B + N + 1) - B - 1;
for (int i = 1; i <= N; ++i) Y[i][j] = std::lower_bound(B + 1, B + C + 1, A[i][j]) - B;
}
for (int i = 1; i <= N; ++i, puts(""))
for (int j = 1; j <= M; ++j)
printf("%d ", std::max(X[i][j], Y[i][j]) + std::max(C1[i] - X[i][j], C2[j] - Y[i][j]));
return 0;
}
【B】Camp Schedule
題意簡述:
有兩個 01 串 \(s\) 和 \(t\),要求重新排列 \(s\) 使得 \(t\) 在重新排列后的 \(s\) 中出現次數盡量多(位置相交的出現也算)。
題解:
貪心匹配 \(t\),隨便用什么方法算出 \(t\) 的最長真公共前后綴,我用了 KMP,然后貪心能選就選。
#include <cstdio>
#include <cstring>
const int MN = 500005;
int N, M, c0, c1;
char s[MN], t[MN];
int p[MN];
int main() {
scanf("%s%s", s + 1, t + 1);
N = strlen(s + 1), M = strlen(t + 1);
for (int i = 1; i <= N; ++i)
if (s[i] == '0') ++c0;
else ++c1;
int k = 0;
for (int i = 2; i <= M; ++i) {
while (k && t[k + 1] != t[i]) k = p[k];
if (t[k + 1] == t[i]) p[i] = ++k;
}
k = 0;
for (int i = 1; i <= N; ++i) {
int r = t[k + 1] - '0';
int q = r ? c1 ? 1 : 0 : c0 ? 0 : 1;
if (q == r) if (++k == M) k = p[M];
--(q ? c1 : c0);
printf("%d", q);
}
return 0;
}
【C】Museums Tour
題意簡述:
一個 \(n\) 個點 \(m\) 條邊的簡單有向圖,一周有 \(d\) 天。
每個點都有一個博物館,給出博物館 \(i\) 在一周的第 \(j\) 天的開門情況(\(1\le i\le n\),\(1\le j\le d\))。
這周的第 \(1\) 天,你從 \(1\) 號點開始,每次走一條邊需要花費 \(1\) 天,如果當前點的博物館開着,你就可以進去游覽。
問在足夠長的時間后,你最多可以游覽多少個不同的博物館(一個點的博物館游覽多次只算一次)。
題解:
有兩種做法,一種比較簡單,一種比較復雜,我寫的是復雜的。
考慮 \(\mathrm{f}[i][j]\) 表示一周的第 \(j\) 天在 \(i\) 號點,最終能游覽多少個博物館。
但是圖上有環,轉移比較困難。於是考慮強連通分量縮點。這樣形成一個 DAG。
觀察可以得到,一個縮點后的強連通分量,是擁有自己的“周期”的,即若第 \(j\) 天在這個強連通分量中,第 \(j+x\) 天也能在同一個點上,最小的正整數 \(x\) 便是周期。
這個周期一定是 \(d\) 的因數,因為 \(d\) 是所有強連通分量的總周期,周期可以這樣計算:
-
周期 \(\mathrm{period}\) 初始化為 \(d\)。
-
考慮這個強連通分量的 DFS 樹,記錄每個點的深度 \(\mathrm{dep}\)。
-
對於每一條邊 \(u\to v\),令 \(\mathrm{period}=\gcd(\mathrm{period},\mathrm{dep}[u]-\mathrm{dep}[v]+1)\)。
關於證明,只能感性理解。
這之后就可以 DP 了,DP 的狀態變為 \(\mathrm{f}[i][j]\) 表示 \(i\) 號強連通分量中的時間戳為 \(j\) 的答案。
因為涉及到強連通分量內所有點的時間戳,需要對每個強連通分量設置一個基准時間戳以便互相轉化,所幸我們可以用 \(\mathrm{dep}\bmod \mathrm{period}\) 來當作節點的相對時間戳,基准時間戳為 \(0\)。
DP 時需要處理一個 \(\mathrm{cnt}[j]\) 數組表示當強連通分量時間戳為 \(j\) 時這個強連通分量內開啟的不同博物館個數。DP 時要格外注意不同強連通分量中時間戳的轉換。
時間復雜度 \(\mathcal{O}((n+m)d)\)。
#include <cstdio>
#include <vector>
#include <algorithm>
const int MN = 100005, MD = 55;
int N, M, D;
std::vector<int> G[MN], T[MN];
char s[MD]; int w[MN][MD];
int dfn[MN], low[MN], stk[MN], instk[MN], tp, dfc;
int dep[MN], bel[MN], per[MN], tic[MN], itic[MN], bcnt;
void Tarjan(int u) {
low[u] = dfn[u] = ++dfc;
stk[++tp] = u, instk[u] = 1;
for (auto v : G[u]) {
if (!dfn[v]) {
dep[v] = dep[u] + 1, Tarjan(v);
low[u] = std::min(low[u], low[v]);
}
else if (instk[v]) low[u] = std::min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++bcnt;
for (int x = 0; x != u; --tp) {
x = stk[tp];
bel[x] = bcnt;
T[bcnt].push_back(x);
instk[x] = 0;
}
}
}
int f[MN][MD];
int main() {
scanf("%d%d%d", &N, &M, &D);
for (int i = 1; i <= M; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
}
for (int i = 1; i <= N; ++i)
if (!dfn[i]) dep[i] = 1, Tarjan(i);
for (int i = 1; i <= bcnt; ++i) per[i] = D;
for (int u = 1; u <= N; ++u)
for (auto v : G[u]) if (bel[u] == bel[v])
per[bel[u]] = std::__gcd(per[bel[u]], std::abs(dep[u] - dep[v] + 1));
for (int i = 1; i <= N; ++i)
tic[i] = dep[i] % per[bel[i]], itic[i] = tic[i] ? per[bel[i]] - tic[i] : 0;
for (int i = 1; i <= N; ++i) {
scanf("%s", s);
for (int j = 0; j < D; ++j) w[i][j] = s[j] - '0';
}
for (int id = 1; id <= bcnt; ++id) {
int pr = per[id];
static int cnt[MD];
for (int j = 0; j < pr; ++j) cnt[j] = 0;
for (int j = 0; j < pr; ++j) {
for (auto u : T[id]) for (int k = 0; k < D; k += pr)
if (w[u][(k + tic[u] + j) % D]) { ++cnt[j]; break; }
}
for (int j = 0; j < pr; ++j) {
f[id][j] = cnt[j];
for (auto u : T[id]) {
int ticv = (j + tic[u] + 1) % pr;
for (auto v : G[u]) if (bel[v] != id) {
for (int k = 0; k < D; k += pr)
f[id][j] = std::max(f[id][j], cnt[j] + f[bel[v]][(itic[v] + k + ticv) % per[bel[v]]]);
}
}
}
}
printf("%d\n", f[bel[1]][itic[1]]);
return 0;
}
// 19-03-10 00:18 ~ 19-03-10 00:54
而相對簡單的做法是,將每個點在一周的每天拆成不同的點。這樣有 \(n\times d\) 個點,每個點記作 \(\langle u,j\rangle\) 即 \(u\) 號點在第 \(j\) 天拆出的點(\(1\le u\le n\),\(0\le j<d\))。每個點的權值為 \(0\) 或 \(1\),取決於當天此點的博物館是否開啟。
對於一條邊 \(u\to v\),將其拆成 \(d\) 條邊,連接 \(\langle u,j\rangle\) 和 \(\langle v,(j+1)\bmod d\rangle\)。
這樣再強連通分量縮點后,形成一個 DAG。每個縮點后的強連通分量權值為這個強連通分量中開啟的不同博物館個數,直接求最長路即可。
為什么不會重復統計?考慮重復統計的情況,即存在一條路徑使得 \(\langle u,j_1\rangle\) 可以到達 \(\langle u,j_2\rangle\) 但不在同一個強連通分量中,這是不可能的,因為單獨看這條路徑經過的頂點編號序列,沿着這個序列再重復走 \(d-1\) 遍一定能回到 \(\langle u,j_1\rangle\)。這意味着如果同一頂點拆出的兩點弱連通,它們必然也強連通,所以不需要擔心重復統計的情況。
代碼就不給了,比較簡單。不過這個做法似乎空間翻好幾番,要小心處理。
【D】Cooperative Game
題意簡述:
這是一道交互題。
有一張 \(t+c\)(\(1\le t,c\),\(t+c\le 1000\))個點的有向圖,每個點只有一條出邊。這張圖由一個長度為 \(t\) 的鏈和一個長度為 \(c\) 的環構成。
即從起點(鏈的頂端)走 \(t-1\) 條邊即可訪問到鏈的尾端,從鏈的尾端再走 \(1\) 條邊即可進入環中,記環上的這個點(從鏈進入的第一個點)為 \(\mathrm{finish}\)。
你在起點(鏈的頂端)有 \(10\) 枚棋子,每次你可以令一部分棋子走一條邊,然后交互庫會告訴你哪些棋子在同一個節點內,哪些棋子在不同節點內。
你需要在不超過 \(3(t+c)\) 次操作后讓所有棋子到達 \(\mathrm{finish}\) 並結束程序。
題解:
思路非常巧妙的一題。其實只需要 \(3\) 枚棋子即可完成任務。
考慮先移動 \(2\) 枚棋子,類似 Floyd 判圈算法,以及 Pollad-rho 算法的判圈過程,我們令一枚棋子速度為 \(2\),另一枚速度為 \(1\)。
它們一定會在環上相遇,並且相遇時,較慢的棋子還沒有走完一整個環,令相遇的點為 \(\mathrm{meet}\)。
假設從 \(\mathrm{finish}\) 開始,走 \(x\) 條邊第一次到達 \(\mathrm{meet}\),較慢的棋子走了 \(\mathrm{slow}\) 步,較快的棋子走了 \(\mathrm{fast}\) 步(顯然 \(\mathrm{fast}=2\times\mathrm{slow}\)):
有 \(\mathrm{slow}=t+x\) 和 \(2\times\mathrm{slow}=\mathrm{fast}=t+k\times c+x\)(\(k\in\mathbb{Z}^+\)),整理得:
\(2(t+x)=t+k\times c+x\),即 \(t+x=k\times c\),即 \(t\equiv c-x\pmod{c}\)。
\(c-x\) 即為 \(\mathrm{meet}\) 在環上剩余的步數,即從 \(\mathrm{meet}\) 走 \(c-x\) 步又到達 \(\mathrm{finish}\)。
所以有剩余的步數加上若干倍的 \(c\) 等於 \(t\)。也就是說,如果這時開始,我們每次讓所有棋子同時走一條邊,最終在 \(t\) 步之后就都會到達 \(\mathrm{finish}\),這正是我們要求的。
於是有了下面的代碼:
#include <cstdio>
int t;
inline void gett() {
fflush(stdout);
scanf("%d", &t);
for (int x = t; x; --x) scanf("%*s");
}
int main() {
do {
puts("next 0 1"), gett();
puts("next 0"), gett();
} while (t == 3);
do {
puts("next 0 1 2 3 4 5 6 7 8 9"), gett();
} while (t == 2);
puts("done");
return 0;
}
【E】Train Car Selection
咕