T1
NFLSOJ P2023050961 捉迷藏
首先只需要考慮所有葉子,只要每個葉子都連向了另一個距離超過 \(2\) 的葉子,則符合要求。距離超過 \(2\) 等價於在不同的父親下。則問題變為一堆點,每個點有顏色,同色點間沒有邊,異色點間兩兩有邊,求最大匹配。
結論:設點最多的顏色 \(c\) 有 \(x\) 個點,若 \(x \ge \lceil \frac{n}{2} \rceil\),則答案為 \(n - x\),否則為 \(\lfloor \frac{n}{2} \rfloor\)。前者顯然,後者考慮構造。先把這 \(x\) 個點排成一個環,維護一個指標指向環內任意一個點。遍歷其他所有點,將這個點插入當前指標所指的點的下一位,然後將指標順時針移到下一個顏色為 \(c\) 的點。容易發現這樣得到的環任意兩個相鄰點不同色,兩兩匹配即可。
程式碼
#include <iostream>
#include <algorithm>
using namespace std;
int n;
int head[200005], nxt[400005], to[400005], ecnt;
void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt; }
int f[200005], dfn[200005];
int lf[200005], lcnt;
int deg[200005];
int ncnt;
void dfs1(int x, int fa) {
f[x] = fa;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa)
dfs1(v, x);
}
}
int main() {
freopen("hide.in", "r", stdin);
freopen("hide.out", "w", stdout);
int tc;
cin >> tc;
while (tc--) {
for (int i = 1; i <= n; i++) deg[i] = head[i] = 0;
ecnt = 0;
cin >> n;
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
add(u, v);
add(v, u);
++deg[u], ++deg[v];
}
if (n == 2) {
cout << "-1\n";
continue;
}
ncnt = lcnt = 0;
int rt = 1;
for (int i = 1; i <= n; i++) (deg[i] == 1) ? (lf[++lcnt] = i) : (rt = i);
dfs1(rt, 0);
sort(lf + 1, lf + lcnt + 1, [](int a, int b) { return f[a] < f[b]; });
if (f[lf[1]] == f[lf[lcnt]]) {
cout << -1 << "\n";
continue;
}
int ans = 0;
for (int i = 1, j = 1; i <= lcnt; i = j) {
while (j <= lcnt && f[lf[j]] == f[lf[i]]) ++j;
if (j - i >= (lcnt + 1) / 2) {
ans = j - i;
break;
}
}
if (!ans)
ans = (lcnt + 1) / 2;
cout << ans << "\n";
}
return 0;
}
T2
NFLSOJ P2023050962 塗乳酪
首先注意到所有點會形成一棵基環樹,而我們只關心 \(s\) 所在的弱連通分量。先假設 \(s\) 原本是白色,而我們需要花 \(p_s\) 的代價把它憑空染成黃色,最後再減去這個代價。會發現一個點會被多次染色,顏色就在兩者間交替。首先考慮 \(s\) 不在環內的情況,考慮 \(dp[i][j]\) 表示 \(i\) 的子樹內,\(i\) 被染了 \(j\) 次色的最大收益。容易發現 \(i\) 被染色 \(j\) 次時其所有兒子染色次數都 \(\le j\),所以是好轉移的。當 \(s\) 不在環內時,\(j\) 只能為 \(0 / 1 / 2\)。
然後考慮 \(s\) 所在環大小為 \(2\) 的情況。對 \(s\) 和 \(a_s\) 分別跑一次上一段的樹形 dp,然後由於環長只有 \(2\),\(a_s\) 至多被染色一次。所以情況數很少,直接分討即可。
對於剩下的,會發現可能會有一個黃色的線段在環上一直轉悠。如果在環上每個點寫下這個點的染色次數,會發現任何時刻所有數極差不超過 \(2\),而且任何符合要求的染色次數都可以構造。所以我們先對環上每個點跑那個樹形 dp,然後把第二維開到 \(n\),最後列舉一下 \(s\) 的染色次數,在環上稍微 dp 一下即可。
程式碼
#include <iostream>
#define int long long
using namespace std;
const int inf = 0x7ffffffffffffff;
int n, s;
int a[5005];
int dp[5005][5005];
int c[5005], ccnt;
int head[5005], nxt[5005], to[5005], ecnt;
void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt; }
bool vis[5005];
int w[5005], p[5005];
void dfs(int x) {
for (int i = 1; i <= n; i++) dp[x][i] = ((i & 1) ? w[x] : 0) - p[x] * i;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (!vis[v]) {
dfs(v);
int mx = 0;
for (int j = 1; j <= n; j++) {
mx = max(mx, dp[v][j]);
dp[x][j] = max(dp[x][j], dp[x][j] + mx);
}
}
}
}
signed main() {
freopen("cheese.in", "r", stdin);
freopen("cheese.out", "w", stdout);
cin >> n >> s;
for (int i = 1; i <= n; i++) cin >> w[i];
for (int i = 1; i <= n; i++) cin >> p[i];
for (int i = 1; i <= n; i++) cin >> a[i], add(a[i], i);
for (int i = s; !vis[i]; i = a[i]) {
vis[i] = 1;
c[++ccnt] = i;
}
if (a[c[ccnt]] != s) {
dfs(s);
cout << max(dp[s][1], dp[s][2]) + p[s] << "\n";
return 0;
}
if (ccnt == 2) {
dfs(s);
dfs(a[s]);
cout << max(dp[s][1], max(dp[s][1] + dp[a[s]][1], dp[s][2])) + p[s] << "\n";
return 0;
}
for (int i = 1; i <= ccnt; i++) dfs(c[i]);
int ans = -inf;
for (int i = 1; i <= n; i++) {
int s0, s1, s2;
s0 = s1 = s2 = dp[s][i];
for (int j = ccnt; j > 1; j--) {
(i ^ 1) ? (s2 += dp[c[j]][i - 2]) : 0;
s1 += dp[c[j]][i - 1];
s0 += dp[c[j]][i];
s1 = max(s1, s0);
s2 = max(s2, s1);
}
ans = max(ans, (i ^ 1) ? s2 : s1);
}
cout << ans + p[s] << "\n";
return 0;
}
T3
NFLSOJ P2023050963 玩遊戲
先假設 \(0\) 比 \(1\) 少,否則翻轉序列並反轉 \(0 / 1\)。透過模擬可以發現長度大於 \(1\) 的 \(1\) 連續段會向右移,而 \(0\) 連續段會向左移,當兩個相撞的時候每次兩個的長度都會減少 \(1\) 直到其中一個長度為 \(1\) 為止。我們先找到一個位置使得將 \(0\) 視為 \(-1\) 之後所有字首和非負,並且序列末尾沒有 \(1\)。可以發現一定可以找到這樣的位置。然後按照連續段模擬,每找到一個 \(1\) 連續段就扔到棧裡,找到一個 \(0\) 連續段就跟棧裡的東西撞一撞,抵消掉。最後可以知道剩餘的 \(1\) 連續段的位置和長度。對於剩下的位置 \(01\) 交替即可。然後就是一個 KMP 求迴圈節。把最後一次相撞的結束時間和迴圈節加起來即為所求。
程式碼
#include <iostream>
#include <algorithm>
#include <cassert>
#define int long long
using namespace std;
const int P = 1000000007;
string str;
int n;
struct node {
int s, l;
} stk[10000005];
int sz;
int nxt[10000005];
int ss[10000005];
signed main() {
freopen("game.in", "r", stdin);
freopen("game.out", "w", stdout);
int tc;
cin >> tc;
while (tc--) {
cin >> str;
n = str.size();
int c0 = 0, c1 = 0;
for (int i = 0; i < n; i++) c0 += (str[i] == '0'), c1 += (str[i] == '1');
if (c0 > c1) {
for (int i = 0; i < n; i++) str[i] = '0' + ((str[i] - '0') ^ 1);
reverse(str.begin(), str.end());
}
int s = 0, p = 0, cs = 1;
for (int i = 0; i < n; i++) {
s += (str[i] == '0' ? -1 : 1);
s < cs ? (p = i + 1, cs = s) : 0;
}
if (s == n) {
cout << 1 << "\n";
continue;
}
if (p && p != n) {
string tstr = str;
str = "";
for (int i = p; ; i = (i + 1) % n) {
str += tstr[i];
if (i == p - 1)
break;
}
}
int pos = n - 1;
while (str[pos] == '1') str.pop_back(), --pos;
string asdf;
while (pos + 1 < n) asdf += '1', ++pos;
str = asdf + str;
sz = 0;
int ans = 0;
for (int i = 0, j = 0; i < n; i = j) {
while (j < n && str[j] == str[i]) ++j;
if (j == i + 1)
continue;
if (str[i] == '1')
stk[++sz] = (node) { i, j - i - 1 };
else {
int cl = j - i - 1;
while (cl) {
int val = min(cl, stk[sz].l);
ans = max(ans, (j - stk[sz].s + min(cl - stk[sz].l, stk[sz].l - cl)) / 2 - 1);
stk[sz].l -= val, cl -= val;
(stk[sz].l == 0) ? (--sz) : 0;
}
}
}
for (int i = 1; i <= n; i++) ss[i] = 0;
int cur = 1;
stk[++sz] = (node) { n, -1 };
for (int i = 1; i <= sz; i++) {
stk[i].s += ans;
while (cur < stk[i].s + 1) ss[(cur - 1) % n + 1] = ((cur & 1) != (stk[i].s & 1)), ++cur;
for (int j = 0; j <= stk[i].l; j++)
ss[(stk[i].s + j) % n + 1] = 1;
cur = stk[i].s + stk[i].l + 1 + 1;
}
nxt[0] = -1, nxt[1] = 0;
for (int i = 2, j = 1; i <= n; i++, j++) {
while (j && ss[i] != ss[j]) j = nxt[j - 1] + 1;
nxt[i] = j;
}
ans += (n % (n - nxt[n]) == 0 ? n - nxt[n] : n);
cout << ans % P << "\n";
}
return 0;
}
經典結論。
觀察性質。
按照題意模擬,觀察性質。