【A】Azulejos
題意簡述:
有兩排瓷磚,每排都有 \(n\) 個,每個瓷磚有高度 \(h_i\) 和價格 \(p_i\) 兩種屬性。
你需要分別重新排列這兩排瓷磚,使得同一排的瓷磚滿足價格不降,后一排的瓷磚的高度嚴格大於前一排對應瓷磚的高度。
判斷無解或輸出一種合法方案。
題解:
首先要滿足價格不降,那么先把兩排瓷磚分別按照 \(p_i\) 排序。
如果同一排中的兩個瓷磚 \(p_i\) 不同,那么順序已經確定。但是如果 \(p_i\) 相同,就可以任意交換順序。
也就是說,前后兩排中的瓷磚都被划分為了若干區間,每個區間中的瓷磚的 \(p_i\) 都相同,而且可以任意交換順序。
讓我們從兩排的第一個區間開始考慮,這時有兩種情況。
第一種是前一排的區間長度較短,那么此時前排的區間中的每個瓷磚都需要與后排的區間中的某個瓷磚配對。
那么在保證盡量能構造出解的前提下,最好把更多更優(\(h_i\) 盡量大)的后排的瓷磚留給后續考慮。
這引出一個貪心策略,使用 set
按照 \(h_i\) 為關鍵字維護瓷磚,
對於前排的每個瓷磚,在后排中尋找一個 \(h_i\) 盡量小,但是比當前瓷磚大的瓷磚與其配對,這樣可以保證留給后面的瓷磚盡量優。
第二種是后一排的區間長度較短,相反地,我們對於后排的每個瓷磚尋找前排中 \(h_i\) 盡量大,但是比當前瓷磚小的瓷磚與其配對即可。
這種策略可以保證留給后續考慮的瓷磚盡量優。那么我們只需要對還未配對的瓷磚繼續考慮即可。
不難寫出代碼,時間復雜度為 \(\mathcal{O}(n\log n)\):
#include <cstdio>
#include <algorithm>
#include <set>
const int MN = 500005;
int N, Ans1[MN], Ans2[MN];
struct dat{ int p, h, id; dat() {} dat(int h, int id) : h(h), id(id) {} } a1[MN], a2[MN];
inline bool operator <(dat i, dat j) { return i.h == j.h ? i.id < j.id : i.h < j.h; }
std::set<dat> s1, s2;
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%d", &a1[i].p);
for (int i = 1; i <= N; ++i) scanf("%d", &a1[i].h);
for (int i = 1; i <= N; ++i) scanf("%d", &a2[i].p);
for (int i = 1; i <= N; ++i) scanf("%d", &a2[i].h);
for (int i = 1; i <= N; ++i) a1[i].id = a2[i].id = i;
std::sort(a1 + 1, a1 + N + 1, [](dat i, dat j) { return i.p < j.p; });
std::sort(a2 + 1, a2 + N + 1, [](dat i, dat j) { return i.p < j.p; });
int cnt = 0;
for (int i = 0; i <= N; ++i) {
if (a1[i].p != a1[i + 1].p || a2[i].p != a2[i + 1].p) {
if (s1.size() < s2.size()) {
for (auto j : s1) {
auto it = s2.lower_bound(dat(j.h, 1));
if (it != s2.begin()) {
--it, ++cnt;
Ans1[cnt] = j.id;
Ans2[cnt] = it->id;
s2.erase(it);
}
else return puts("impossible"), 0;
}
s1.clear();
}
else {
for (auto j : s2) {
auto it = s1.upper_bound(dat(j.h, N));
if (it != s1.end()) {
++cnt;
Ans2[cnt] = j.id;
Ans1[cnt] = it->id;
s1.erase(it);
}
else return puts("impossible"), 0;
}
s2.clear();
}
if (a1[i].p != a1[i + 1].p)
for (int j = i + 1; j <= N && a1[j].p == a1[i + 1].p; ++j)
s1.insert(a1[j]);
if (a2[i].p != a2[i + 1].p)
for (int j = i + 1; j <= N && a2[j].p == a2[i + 1].p; ++j)
s2.insert(a2[j]);
}
}
for (int i = 1; i <= N; ++i) printf("%d ", Ans1[i]); puts("");
for (int i = 1; i <= N; ++i) printf("%d ", Ans2[i]); puts("");
return 0;
}
【B】Beautiful Bridges
題意簡述:
給定一個地形剖面圖,用 \(n\)(\(n\le 10^4\))個點描述,點 \(i\) 和點 \(i+1\) 之間有直線連接的地面。
你需要建一座拱橋,連接點 \(1\) 和點 \(n\),橋面的高度為 \(h\)。
你可以在橋中間建若干個柱子,以分配重量,柱子只能恰好建在給出的 \(n\) 個點上(點 \(1\) 和點 \(n\) 上必須有柱子)。
相鄰的兩根柱子之間需要建一個半圓形的拱,准確地說,拱的半徑為兩根柱子之間的距離的一半,並且與兩根柱子和橋面相切。
拱可以與地面相切,但不能相交。
同時,橋的花費與柱子高度和拱面積有關,具體地,給出兩個參數 \(\alpha\) 和 \(\beta\),
則花費為 \(\alpha\sum_{i=1}^{k}h_i+\beta\sum_{i=1}^{k-1}d_i^2\),其中 \(k\) 為柱子數量,\(h_i\) 為第 \(i\) 個柱子的高度,\(d_i\) 為第 \(i\) 個柱子到第 \(i+1\) 個柱子的距離。
問是否可以建出橋,若可以,問最小花費。
題解:
因為 \(n\) 不大,我們猜測正解是 \(\mathcal{O}(n^2)\) 的。
考慮一個簡單的 DP:\(\mathrm{f}[i]\) 表示從 \(1\) 號點連接到 \(i\) 號點的最小花費,若不可行則為 \(\infty\)。
那么 \(\mathrm{f}[i]\) 可以從任意一個合法的 \(\mathrm{f}[j]\)(\(1\le j<i\))轉移而來,多出的花費為 \(\alpha(h-y_i)+\beta(x_i-x_j)^2\)。
這樣總轉移數就是 \(\mathcal{O}(n^2)\) 的,但是要判斷一個轉移是否合法還需要 \(\mathcal{O}(i-j)\) 的時間,總復雜度提高到 \(\mathcal{O}(n^3)\)。
這時有兩種優化思路,從轉移數或判斷合法性入手均可,但是觀察發現合法轉移點無明顯規律,所以考慮如何優化判斷合法性。
一個 \(j\) 到 \(i\) 的轉移是否合法,取決於 \(j\) 到 \(i\) 之間的每個點是否和拱形相交。
注意到 \(i\) 固定,考慮利用這個性質,對每個 \(j\) 判斷轉移是否合法。
可以發現,對於每個 \(j\) 點,拱形的右端點固定時,此點不會與拱形相交的左端點形成一個區間,而區間本身可以通過解方程得到。
從右往左考慮每個 \(j\) 點,將它們對應的區間並起來便可以得到合法區間,對與每個轉移只要判斷 \(x_j\) 是否在合法區間內部即可。
據此寫出代碼,復雜度 \(\mathcal{O}(n^2)\)。
#include <cstdio>
#include <cmath>
#include <algorithm>
typedef long long LL;
const LL Inf = 0x3f3f3f3f3f3f3f3f;
const int MN = 10005;
inline LL MySqrt(LL x) {
LL y = sqrt(x);
while (y * y > x) --y;
while ((y + 1) * (y + 1) <= x) ++y;
return y;
}
int N;
LL H, Alpha, Beta;
LL px[MN], py[MN];
LL f[MN];
int main() {
scanf("%d%lld%lld%lld", &N, &H, &Alpha, &Beta);
for (int i = 1; i <= N; ++i) scanf("%lld%lld", &px[i], &py[i]);
f[1] = Alpha * (H - py[1]);
for (int i = 2; i <= N; ++i) {
f[i] = Inf;
LL Lb = px[i] - 2 * (H - py[i]), Rb = px[i];
for (int j = i - 1; j >= 1; --j) {
LL C1 = px[i] - px[j], C2 = H - py[j];
LL Sqrt = MySqrt(8 * C1 * C2);
LL MIN = px[i] - 2 * (C1 + C2) - Sqrt;
LL MAX = px[i] - 2 * (C1 + C2) + Sqrt;
if (px[i] - px[j] <= 2 * (H - py[j])) MAX = px[j];
Lb = std::max(Lb, MIN);
Rb = std::min(Rb, MAX);
if (Lb <= px[j] && px[j] <= Rb)
f[i] = std::min(f[i], f[j] + Alpha * (H - py[i]) + Beta * (px[i] - px[j]) * (px[i] - px[j]));
}
}
if (f[N] != Inf) printf("%lld\n", f[N]);
else puts("impossible");
return 0;
}
【C】Checks Post Facto
待補
【D】Circular DNA
題意簡述:
一個長度為 \(n\) 的環形 DNA 序列,以順時針順序給出,其中每個基因有類型和編號兩個屬性,類型是 s
(頭)或 e
(尾)中的一種,而編號是 \(1\) 到 \(10^6\) 中的整數。
你需要在某個地方切斷,按照順時針順序拉成鏈后,最大化能夠完美匹配的基因編號個數。
一個基因編號 \(i\) 是能夠完美匹配的,當且僅當它在鏈中對應的所有基因,將 s
看作左括號,e
看作右括號,可以匹配成非空的合法括號序列。
如果有多個位置滿足最大化的條件,輸出最小的位置。
題解:
考慮每個基因編號,首先左括號個數要等於右括號個數,然后把左括號看作 \(+1\),右括號看作 \(-1\),在環上走一遍形成一個折線圖,只有從其最低點開始,才能形成合法括號序。
那么把所有最低點(可能有多個)左右兩邊的 e
和 s
之間的位置都是可行的。
也就是區間加 \(1\),最后查詢全局,差分-前綴和即可。
不難寫出代碼,復雜度 \(\mathcal{O}(n+m)\),\(m\) 為值域范圍:
#include <cstdio>
#include <vector>
inline void getStr(int &Typ, int &Idt) {
char ch; Idt = 0;
while ((ch = getchar()) != 'e' && ch != 's') ;
Typ = ch == 's' ? 1 : -1, ch = getchar();
while (Idt = Idt * 10 + (ch ^ '0'), (ch = getchar()) >= '0' && ch <= '9') ;
}
const int MN = 1000005;
const int M = 1000000;
int N;
int Ty[MN], Id[MN], S[MN], Ans[MN];
std::vector<int> G[MN];
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i)
getStr(Ty[i], Id[i]),
G[Id[i]].push_back(i);
for (int id = 1; id <= M; ++id) {
int Sum = 0, Mn = 0;
for (auto i : G[id]) {
Sum += Ty[i];
S[i] = Sum;
if (Mn > Sum) Mn = Sum;
}
if (Sum) continue;
for (int i = 0; i < (int)G[id].size(); ++i) {
if (S[G[id][i]] == Mn) {
if (i < (int)G[id].size() - 1)
++Ans[G[id][i] + 1], --Ans[G[id][i + 1] + 1];
else {
++Ans[G[id][i] % N + 1], --Ans[G[id][0] + 1];
if (G[id][i] != N) ++Ans[1];
}
}
}
}
for (int i = 1; i <= N; ++i) Ans[i] += Ans[i - 1];
int Ai = 1, Av = Ans[1];
for (int i = 2; i <= N; ++i)
if (Ans[i] > Av) Ai = i, Av = Ans[i];
printf("%d %d\n", Ai, Av);
return 0;
}
【E】Dead-End Detector
題意簡述:
有一張 \(n\) 個點 \(m\) 條邊的簡單無向圖。
如果走過一條邊 \(u\to v\) 后,不掉頭無法返回到 \(u\),這條邊就是對 \(u\) 來說的“死路”。
你需要對每個死路標記路標,但是有的路標是多余的。
如果從一個死路 \(u\to v\) 開始可以不掉頭地走到另一個死路 \(u'\to v'\),那么后者 \(u'\to v'\) 就是多余的。
最后問要標記多少路標,輸出每對 \(u\to v\),按照 \(u\) 為第一關鍵字,\(v\) 為第二關鍵字排序。
題解:
對每個連通塊分別考慮。
如果當前連通塊無環,也就是一棵樹,那么所有方向的邊都是死路,但是為了防止多余,應該在所有葉子(度數為 \(1\) 的點)處放置路標。
如果當前連通塊有環(這里的環指的是不掉頭,並且首尾相接的路徑),那么環內的邊都不是死路,指向環的邊也不是死路,只有遠離環的邊才是死路,為了防止多余,應該在所有離開環的邊處放置路標。
維護一個隊列,每次刪除度數為 \(1\) 的點,不難區分這些情況。
不難寫出代碼,復雜度 \(\mathcal{O}(n+m)\):
#include <cstdio>
#include <algorithm>
#include <vector>
const int MN = 500005;
int N, M;
std::vector<int> G[MN];
int d[MN];
int vis[MN], que[MN], l, r;
int Ans, A1[MN], A2[MN];
int main() {
scanf("%d%d", &N, &M);
for (int i = 1; i <= M; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
++d[u], ++d[v];
}
for (int i = 1; i <= N; ++i)
std::sort(G[i].begin(), G[i].end());
l = 1, r = 0;
for (int i = 1; i <= N; ++i)
if (d[i] == 1) vis[i] = 1, que[++r] = i;
while (l <= r) {
int u = que[l++];
for (auto v : G[u]) {
if (!vis[v] && --d[v] == 1)
vis[v] = 1, que[++r] = v;
}
}
l = 1, r = 0;
for (int i = 1; i <= N; ++i) vis[i] = 0;
for (int i = 1; i <= N; ++i)
if (d[i] > 1) vis[i] = 1, que[++r] = i;
while (l <= r) {
int u = que[l++];
for (auto v : G[u]) {
if (!vis[v])
vis[v] = 1, que[++r] = v;
}
}
for (int u = 1; u <= N; ++u) {
for (auto v : G[u]) {
if (!vis[u] && G[u].size() == 1)
A1[++Ans] = u, A2[Ans] = v;
if (vis[u] && d[u] > 1 && d[v] == 1)
A1[++Ans] = u, A2[Ans] = v;
}
}
printf("%d\n", Ans);
for (int i = 1; i <= Ans; ++i)
printf("%d %d\n", A1[i], A2[i]);
return 0;
}
【F】 Directing Rainfall
待補
【G】First of Her Name
題意簡述:
給定一個 \(n+1\) 個點的 trie 樹,根節點表示空串,非根節點表示的字符串為其父節點表示的字符串在前端加上父節點到它的邊上的字符。
給定 \(k\) 個詢問,每次給出一個字符串,詢問 trie 樹中 \(n\) 個非根節點表示的字符串中有多少個以詢問串作為前綴。
題解:
對詢問串的反串建立 AC 自動機,把原 trie 樹在自動機上跑,每次跑到的位置在 \(\mathrm{fail}\) 樹上的權值加 \(1\)。每個詢問的答案就是 \(\mathrm{fail}\) 樹上子樹權值和。
注意 AC 自動機需要拓展轉移,否則不斷跳 \(\mathrm{fail}\) 會讓復雜度失去保證。
子樹權值和可以直接最后統計,但是我使用了樹狀數組,復雜度 \(\mathcal{O}(n\log n)\)
據此寫出代碼,復雜度 \(\mathcal{O}(n\Sigma+n\log n)\):
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
const int MN = 1000005;
int N, Q;
int h[MN], nxt[MN], to[MN], w[MN], tot;
inline void Ins(int x, int y, int z) {
nxt[++tot] = h[x], to[tot] = y, w[tot] = z, h[x] = tot;
}
char str[MN];
int ch[MN][26], fail[MN], cnt;
inline int Insert(char *str) {
int now = 0;
for (int i = 0; str[i]; ++i) {
int c = str[i] - 'A';
if (!ch[now][c]) ch[now][c] = ++cnt;
now = ch[now][c];
} return now;
}
std::vector<int> G[MN];
int que[MN], l, r;
void BuildAC() {
fail[0] = -1;
que[l = r = 1] = 0;
while (l <= r) {
int u = que[l++];
for (int j = 0; j < 26; ++j) {
if (ch[u][j]) {
int to = fail[u];
while (~to && !ch[to][j]) to = fail[to];
fail[ch[u][j]] = ~to ? ch[to][j] : 0;
que[++r] = ch[u][j];
}
else ch[u][j] = ~fail[u] ? ch[fail[u]][j] : 0;
}
}
for (int i = 1; i <= cnt; ++i) G[fail[i]].push_back(i);
}
int ldf[MN], rdf[MN], dfc;
void DFS0(int u) {
ldf[u] = ++dfc;
for (auto v : G[u]) DFS0(v);
rdf[u] = dfc;
}
int b[MN];
inline void Mdf(int i) { for (; i <= dfc; i += i & -i) ++b[i]; }
inline int Qur(int i) { int a = 0; for (; i; i -= i & -i) a += b[i]; return a; }
void Solve(int u, int now) {
Mdf(ldf[now]);
for (int i = h[u]; i; i = nxt[i])
Solve(to[i], ch[now][w[i]]);
}
int Pos[MN];
int main() {
scanf("%d%d", &N, &Q);
for (int i = 1; i <= N; ++i) {
int f; char ch[3];
scanf("%s%d", ch, &f);
Ins(f, i, *ch - 'A');
}
for (int i = 1; i <= Q; ++i) {
scanf("%s", str);
std::reverse(str, str + strlen(str));
Pos[i] = Insert(str);
}
BuildAC();
DFS0(0);
Solve(0, 0);
for (int i = 1; i <= Q; ++i)
printf("%d\n", Qur(rdf[Pos[i]]) - Qur(ldf[Pos[i]] - 1));
return 0;
}
【H】Hobson's Trains
題意簡述:
給定一個 \(n\) 個點的圖,每個點 \(i\) 都有且僅有一條連向 \(d_i\) 的有向邊,即一個基環內向森林。
給定 \(k\),對於每個點,回答沿着有向邊走不超過 \(k\) 步能到達這個點的點數。
題解:
找出環,對於環里面的樹和環本身分開考慮。
對於樹的部分,可以樹上差分,DFS 解決。
對於環的部分,考慮每個點對環的貢獻,每個點影響到的點是環上的一個連續段,具體位置和長度有關它到達環上的第一個點和到環的距離。
也用差分-前綴和方法統計即可。
雖然思路很簡單,細節還是有一些的。
據此寫出代碼,復雜度 \(\mathcal{O}(n)\)。
#include <cstdio>
#include <vector>
const int MN = 500005;
int N, K, d[MN];
int inc[MN], vis[MN], ist[MN], stk[MN], tp;
int cid, Len[MN], Id[MN];
std::vector<int> C[MN], sum[MN];
void Circ(int u) {
stk[++tp] = u, vis[u] = tp, ist[u] = 1;
if (!vis[d[u]]) Circ(d[u]);
else if (ist[d[u]]) {
++cid;
for (int i = vis[d[u]]; i <= tp; ++i)
C[cid].push_back(stk[i]),
inc[stk[i]] = cid,
Id[stk[i]] = (int)C[cid].size() - 1;
Len[cid] = C[cid].size();
sum[cid].resize(Len[cid]);
}
--tp, ist[u] = 0;
}
std::vector<int> G[MN];
int tc[MN], dep[MN], S[MN];
void DFS(int u) {
stk[++tp] = u, dep[u] = tp;
tc[u] = tc[d[u]];
++S[u];
if (tp > K + 1) --S[stk[tp - K - 1]];
for (auto v : G[u]) DFS(v), S[u] += S[v];
--tp;
}
int main() {
scanf("%d%d", &N, &K);
for (int i = 1; i <= N; ++i) scanf("%d", &d[i]);
for (int i = 1; i <= N; ++i) if (!vis[i]) Circ(i);
for (int i = 1; i <= N; ++i) if (!inc[d[i]]) G[d[i]].push_back(i);
for (int i = 1; i <= N; ++i) if (inc[i]) tc[i] = i;
for (int i = 1; i <= N; ++i) if (!inc[i] && inc[d[i]]) DFS(i);
for (int i = 1; i <= N; ++i) {
if (dep[i] > K) continue;
if (K - dep[i] + 1 >= Len[inc[tc[i]]])
++sum[inc[tc[i]]][0];
else {
++sum[inc[tc[i]]][Id[tc[i]]];
int ed = (Id[tc[i]] + K - dep[i] + 1) % Len[inc[tc[i]]];
--sum[inc[tc[i]]][ed];
if (ed < Id[tc[i]]) ++sum[inc[tc[i]]][0];
}
}
for (int id = 1; id <= cid; ++id) {
S[C[id][0]] = sum[id][0];
for (int i = 1; i < Len[id]; ++i)
sum[id][i] += sum[id][i - 1],
S[C[id][i]] = sum[id][i];
}
for (int i = 1; i <= N; ++i) printf("%d\n", S[i]);
return 0;
}
【I】Karel the Robot
待補
【J】Miniature Golf
待補
【K】Traffic Blights
待補