20240503

forgotmyhandle發表於2024-05-04

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;
}

經典結論。

觀察性質。

按照題意模擬,觀察性質。