Say 题选记(10.5 - 10.11)

P4797 [CEOI 2015] 波将金的路径

题目要我们找一个环长 \(\ge 4\) 的环,使得这个环没有弦。难点显然在这个没有弦的限制。如果我们直接找最小环,可能会找到一个三元环,虽然没有弦了,但也不满足题目的要求。
一个人类智慧的点边转换,考虑对边之间连边。初步想法是将 \((u, v)\)\((v, w)\) 连边,条件是不存在边 \((u,w)\),这样就手动把三元环排除掉了。可以发现,直接在新图上找最小环(肯定没有弦)就行了。
但这样还是有问题,如果按照无向边来处理的话,如果原图是菊花图,建出来的新图依然有环。那么我们把原图的无向边拆成有向边,还是按照上面的方法建新图,这样新图上的环和原图上的非三元环存在一一对应关系。
最小环跑 \(floyd\)?没必要。仔细思考我们只是要找到一个极小环,直接 dfs,然后处理返祖边就行。具体来说,从上到下 dfs,找到 \(u\) 的返祖边中深度最大的那个(如果存在),直接返回就行,这样找到的一定是极小环。因为如果还有更小的环的话在 \(u\) 的祖先处就被遍历到了。
复杂度在于建图的 \(O(N^3)\)

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 5, M = 2e5 + 5;
int n, m, g[N][N], ver[M][2], dep[M], st[M], tp;
vector<int> e[M];
bitset<M> in;
void dfs(int u, int fa){
    dep[u] = dep[fa] + 1;
    in[st[++tp] = u] = 1;
    int x = 0;
    for(int v : e[u]){
        if(in[v] && dep[x] < dep[v]) x = v;
    }
    if(x){
        cout << ver[u][1] << ' ';
        --tp;
        while(1){
            cout << ver[st[tp]][1] << ' ';
            if(st[tp] == x) exit(0);
            --tp;
        }
    }
    for(int v : e[u]){
        if(!dep[v]) dfs(v, u);
    }
    in[st[tp--]] = 0;
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> m;
    for(int i = 1; i <= m; ++i){
        int &u = ver[i][0], &v = ver[i][1];
        cin >> u >> v;
        g[u][v] = i;
        g[v][u] = m + i;
        ver[m + i][0] = v, ver[m + i][1] = u;
    }
    for(int v = 1; v <= n; ++v){
        for(int u = 1; u <= n; ++u){
            for(int w = u + 1; w <= n; ++w){
                if(g[u][v] && g[v][w] && !g[u][w]){
                    e[g[u][v]].emplace_back(g[v][w]);
                    e[g[w][v]].emplace_back(g[v][u]);
                }
            }
        }
    }
    for(int i = 1; i <= 2 * m; ++i){
        if(!dep[i]) dfs(i, 0);
    }
    cout << "no";
    return 0;
}

P6620 [省选联考 2020 A 卷] 组合数问题

普通幂转下降幂的 trick。
为什么要引入下降幂呢,主要是由于求导的性质,如果 \(f(k)\) 是关于 \(k\) 的函数。
并且 \(F(x) = \sum_k f(k)x ^k\)
两边求 \(i\) 阶导,\(F^{(i)}(x) = \sum_k f(k)k^{\underline{i}} x ^ {k - i}\)
也就是说,如果我们知道 \(F(x)\) 的封闭形式,那么推广到 \(x\) 的系数是 \(k\) 的下降幂多项式的形式的幂级数也是有封闭形式的。

考虑如何把普通幂多项式转化成下降幂多项式。首先要知道一个式子 \(m^n = \sum_{i = 0}^m {n \brace i}m^{\underline{i}}\)。证明的话考虑组合意义,都是把 \(n\) 个有标号的球放进 \(m\) 的有标号的盒子里。
那么对于 \(\sum_{i = 0}^m a_ix^i\)\(m\) 次多项式,套用刚刚的方法,就变成了 \(\sum_{i = 0}^m a_i(\sum_{k = 0}^i {i \brace k}x^{\underline{k}})\),整理一下,也就是 \(\sum_{i = 0}^m (\sum_{j = i}^m a_j{j \brace i})x^{\underline{i}}\)

对于原式子 \(\sum_{k = 0}^n G(k){n \choose k}x^k\)。其中 \(G(k)\) 是关于 \(k\)\(m\) 次多项式。由于 \(F(x) = \sum_{k = 0}^n{n \choose k}x^k\) 有封闭形式 \(F(x) = (1 + x)^n\)。考虑对其两边求 \(i\) 阶导,也就是说 \(\sum_{k = 0}^n {n \choose k} k^{\underline{i}} x^{k - i} = n^{\underline{i}}(1 + x)^{n - i}\)。两边同时乘 \(x^i\)\(\sum_{k = 0}^n {n \choose k} k^{\underline{i}} x^{k} = n^{\underline{i}}(1 + x)^{n - i}x^i\)。用刚刚的方法把 \(G(k)\) 转成 \(\sum_{t = 0}^m b_t\times k^{\underline{t}}\) 的形式,整理求和号 \(\sum_{k = 0}^n (\sum_{i = 0}^m b_i\times k^{\underline{i}}) {n \choose k}x^k = \sum_{i = 0}^m b_i (\sum_{k = 0}^n {n \choose k}k^{\underline{i}} x^k) = \sum_{i = 0}^m b_i n^{\underline{i}}(1 + x)^{n - i}x^i\) 就做完了。

递推斯特林数时,注意一下初值怎么赋。\({i \brace 0} = [i = 0]\)

Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int M = 1e3 + 5;
int mod, x, n, m;
ll b[M], a[M], f[M][M];
ll qpow(ll a, ll b){
    ll ret = 1;
    for(int x = b; x; x >>= 1, (a *= a) %= mod){
        if(x & 1) (ret *= a) %= mod;
    }
    return ret;
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> x >> mod >> m;
    for(int i = 0; i <= m; ++i) cin >> a[i];
    f[0][0] = 1;
    for(int i = 0; i <= m; ++i){
        for(int j = 1; j <= i; ++j){
            f[i][j] = (f[i - 1][j - 1] + j * f[i - 1][j]) % mod;
        }
    }
    for(int i = 0; i <= m; ++i){
        for(int j = i; j <= m; ++j)
            (b[i] += a[j] * f[j][i]) %= mod;
    }
    ll ans = 0, tmp = 1;
    for(int i = 0; i <= m; ++i){
        (ans += b[i] * tmp % mod * qpow(x, i) % mod * qpow(x + 1, n - i) % mod) %= mod;
        (tmp *= (n - i)) %= mod;
    }
    cout << ans;
    return 0;
}

P6624 [省选联考 2020 A 卷] 作业题

先欧拉反演 \(\gcd\) 去了,\(val(T) = (\sum_{i = 1}^{n - 1} w_{e_i})\sum_d [d|w_{e_1}][d|w_{e_2}] \cdots [d|w_{e_n}] \varphi(d)\),这一步莫反也行。
考虑枚举 \(d\),也就是只能选所有 \(d\) 的倍数的边,求所有生成树边权之和,以 \(\varphi(d)\) 的系数贡献到答案。

然后跑去看了 oi-wiki 矩阵树定理的证明(以前怎么没发现实际上写得挺清晰的)。首先是行列式的组合意义(两列点奇偶逆序对什么的)。还有就是关联矩阵刻画了树的限制,也就是无环并且联通。再加上关联矩阵按照一些方式相乘又可以变成邻接矩阵、度数矩阵什么的。最后就得到了矩阵树定理的带权形式。也就是可以求出 所有生成树边权的积 的和。

可是本题让我们求的是生成树边权的和啊。那么就可以考虑多项式操作(乘转加)了,把一条边的边权看成 \(wx + 1\),生成树边权之和就是一次项系数,更高次可以直接丢掉了(\(\mod x\) 意义下)。高斯消元什么的都一样,只是把实数换成了多项式罢了,注意一下多项式求逆。

实现时,如果发现 \(d\) 的倍数的边的数量不足 \(n - 1\),那就不用跑了,这样就稳过。

Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 998244353, N = 35, V = 152515;
int n, phi[V], g[V], buc[V], pri[V], cnt, m;
bitset<V> vis;
struct poly{
    int a, b;
}L[N][N];
void init(int v){
    phi[1] = 1;
    for(int i = 2; i <= v; ++i){
        if(!vis[i]){
            pri[++cnt] = i;
            phi[i] = i - 1;
            g[i] = i;
        }
        for(int j = 1; j <= cnt; ++j){
            int nxt = i * pri[j];
            if(nxt > v) break;
            vis[nxt] = 1;
            if(i % pri[j] == 0){
                g[nxt] = g[i] * pri[j];
                if(nxt == g[nxt]) phi[nxt] = phi[i] * pri[j];
                else phi[nxt] = phi[g[nxt]] * phi[nxt / g[nxt]];
                break; 
            }
            g[nxt] = pri[j];
            phi[nxt] = phi[pri[j]] * phi[i];
        }
    }
}
struct edge{
    int u, v; poly w;
}e[N * N];
int qpow(int a, int b){
    int ret = 1;
    for(; b; b >>= 1, (a *= a) %= mod)
        if(b & 1) (ret *= a) %= mod;
    return ret;
}
poly operator + (poly x, poly y){ return {(x.a + y.a) % mod, (x.b + y.b) % mod}; }
poly operator - (poly x, poly y){ return {(mod + x.a - y.a) % mod, (mod + x.b - y.b) % mod}; }
poly operator * (poly x, poly y){ 
    return {(x.a * y.b + x.b * y.a) % mod, 
        (x.b * y.b) % mod}; 
}
poly inv(poly x){
    int Inv = qpow(x.b, mod - 2);
    return {-x.a * Inv % mod * Inv % mod, Inv};
}

int det(poly a[N][N], int n){
    int fl = 0;
    for(int i = 1; i <= n; ++i){
        int r = 0;
        for(int j = i; j <= n; ++j){
            if(a[j][i].b){ r = j; break; } 
        }
        if(!r) continue;
        if(r != i) swap(a[i], a[r]), fl ^= 1;
        poly tmp = inv(a[i][i]);
        for(int j = i + 1; j <= n; ++j){
            poly c = a[j][i] * tmp; 
            for(int k = i; k <= n; ++k){
                a[j][k] = a[j][k] - c * a[i][k];
            }
        }
    }
    poly ret = {0, (fl ? mod - 1 : 1)};
    for(int i = 1; i <= n; ++i) ret = ret * a[i][i];
    return ret.a;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> m;
    int mx = 0;
    for(int i = 1; i <= m; ++i){
        int u, v, w;
        cin >> u >> v >> w;
        mx = max(mx, w);
        e[i] = {u, v, poly{w, 1}};
        for(int j = 1; j <= sqrt(w); ++j){
            if(w % j == 0){
                buc[j]++;
                if(j * j != w) buc[w / j]++;
            }
        }
    }
    init(mx);
    int ans = 0;
    for(int d = 1; d <= mx; ++d){
        if(buc[d] >= n - 1){
            memset(L, 0, sizeof(L));
            for(int i = 1; i <= m; ++i){
                int u = e[i].u, v = e[i].v, c = e[i].w.a;
                if(c % d == 0){
                    L[u][v] = L[u][v] - e[i].w;
                    L[v][u] = L[v][u] - e[i].w;
                    L[u][u] = L[u][u] + e[i].w;
                    L[v][v] = L[v][v] + e[i].w;
                }
            }
            int ret = det(L, n - 1);
            (ans += phi[d] * ret % mod) %= mod;
        }
    }
    cout << ans;
    return 0;
}

P6628 [省选联考 2020 B 卷] 丁香之路

把要求必须经过的边的端点、起点和终点成为关键点。显然我们可以只考虑关键点。
要求一些边经过至少一次可以转换为欧拉回路问题。具体来说,可以考虑加边,使得整张图存在 \(s \to i\) 的欧拉路径,欧拉路径不好处理,可以再加一条 \(i \to s\) 变成欧拉回路。这样一条边经过多次或者经过一些不必须经过的边相当于多加了一些边。题目要求最小化加边代价。
欧拉回路与度数有很大关系,考虑把要求必须经过的边全都加进去,然后进行调整。由于总的度数是 \(2 \times m\),我们总可以把那些奇度点之间配对连边,使得他们都变成偶度。由于代价是 \(|i - j|\),排序之后相邻两两配对即可。
但是发现这样调整完之后原题可能变为几个联通块。这样的话我们再跑一遍最小生成树就可以了。由于原图性质特殊,相邻关键点之间两两连边一定形成一个生成树了,那么我们跑 MST 的时候只考虑这些边就行。
既然最后可能分为几个联通块,那第二步连边的时候我们可以把两个奇度点之间的偶度点也顺便全部连上,这对度数的奇偶性没有任何影响,却让更多的点联通在了一起。
复杂度 \(O(N^2 \log N+M)\)

Code
#include <bits/stdc++.h>
using namespace std;
typedef tuple<int, int> pii;
const int N = 2505;
int deg[N], n, m, s, p[N];
vector<pii> e[N];
bitset<N> bs;
struct DSU{
    int fa[N];
    void init(){ iota(fa + 1, fa + 1 + n, 1); }
    int getf(int u){ return (fa[u] == u ? u : fa[u] = getf(fa[u])); }
    void merge(int u, int v){
        u = getf(u), v = getf(v);
        if(u != v) fa[u] = v;
    }
}tr, otr;
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> m >> s;
    bs[s] = 1; deg[s]++;
    otr.init();
    int ori = 0;
    for(int i = 1; i <= m; ++i){
        int u, v;
        cin >> u >> v;
        otr.merge(u, v);
        ori += abs(u - v);
        bs[u] = bs[v] = 1;
        deg[u]++, deg[v]++;
    }
    for(int i = 1; i <= n; ++i){
        bs[i] = 1;
        deg[i]++;
        int tot = 0, ans = ori;
        tr = otr;
        for(int j = 1; j <= n; ++j){
            if(bs[j]) p[++tot] = j;
        }
        int lst = 0;
        for(int j = 1; j <= tot; ++j){
            int x = p[j];
            if(deg[x] & 1){
                if(!lst) lst = j;
                else{
                    ans += x - p[lst];
                    for(int k = lst; k < j; ++k) tr.merge(p[k], x);
                    lst = 0;
                }
            }
        }
        for(int j = 1; j < tot; ++j){
            e[p[j + 1] - p[j]].emplace_back(p[j], p[j + 1]);
        }
        for(int w = 1; w <= n; ++w){
            for(auto [u, v] : e[w]){
                int x = tr.getf(u), y = tr.getf(v);
                if(x != y){
                    ans += 2 * w;
                    tr.merge(x, y);
                }
            }
            e[w].clear();
        }
        cout << ans << ' ';
        deg[i]--;
        if(!deg[i]) bs[i] = 0;
    }
    return 0;
}

P3237 [HNOI2014] 米特运输

难点在于屎一般题意的转化。

Code
#include <bits/stdc++.h>
using namespace std;
using ull = unsigned long long;
const int N = 5e5 + 5, mod = 1e9 + 7;
map<pair<ull, ull>, int> mp;
ull w[N], f[N], f2[N];
vector<int> e[N];
int n;
void dfs(int u, int fa){
    if(u == 1) f[u] = f2[u] = 1;
    else f[u] = f[fa] * e[fa].size(), f2[u] = (f2[fa] * e[fa].size()) % mod;
    mp[{w[u] * f[u], (w[u] * f2[u]) % mod}]++;
    for(int v : e[u]){
        dfs(v, u);
    } 
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n;
    for(int i = 1; i <= n; ++i) cin >> w[i];
    for(int i = 1; i < n; ++i){
        int u, v;
        cin >> u >> v;
        e[u].emplace_back(v);
    }
    dfs(1, 0);
    int mx = 0;
    for(auto [x, y] : mp){
        mx = max(mx, y);
    }
    cout << n - mx;
    return 0;
}

P5283 [十二省联考 2019] 异或粽子

超级钢琴的异或和版本。查询区间异或最大值的部分需要用可持久化 \(0-1\) trie,写起来也是照猫画虎就行。

Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using tpi = tuple<ll, int, int, int, int>;
const int N = 5e5 + 5, D = 35;
int n, k, rt[N], cnt, tr[N * D][2], num[N * D], ed[N * D];
ll a[N], sum[N];
void copy(int x, int y) {
    tr[x][0] = tr[y][0], tr[x][1] = tr[y][1];
    num[x] = num[y];
}
int ins(int pre, ll x, int idx) {
    int p = ++cnt, ret = p;
    copy(p, pre);
    num[p]++;
    for (int i = 32; i >= 0; --i) {
        int s = (x >> i) & 1;
        tr[p][s] = ++cnt;
        p = tr[p][s], pre = tr[pre][s];
        copy(p, pre);
        num[p]++;
    }
    ed[p] = idx;
    return ret;
}
pair<ll, int> query(int l, int r, ll y) {
    ll ret = 0;
    int v = rt[r], u = (l == 0 ? 0 : rt[l - 1]);
    for (int i = 32; i >= 0; --i) {
        int s = ((y >> i) & 1) ^ 1;
        if (num[tr[v][s]] - num[tr[u][s]]) { 
            ret += (1ll << i);
            u = tr[u][s], v = tr[v][s];
        } else
           u = tr[u][s ^ 1], v = tr[v][s ^ 1];
    }
    return {ret, ed[v]};
}
priority_queue<tpi> q;
int main() {
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> k;
    rt[0] = ins(0, 0, 0);
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        sum[i] = sum[i - 1] ^ a[i];
        rt[i] = ins(rt[i - 1], sum[i], i);
    }
    for (int i = 1; i <= n; ++i) {
        auto [ret, pos] = query(0, i - 1, sum[i]);
        q.emplace(ret, 0, i - 1, pos, i);
    }
    ll ans = 0;
    while (k--) {
        auto [dat, l, r, pos, idx] = q.top();
        q.pop();
        ans += dat;
        if (l < pos) {
            auto [ret, nxt] = query(l, pos - 1, sum[idx]);
            q.emplace(ret, l, pos - 1, nxt, idx);
        }
        if (r > pos) {
            auto [ret, nxt] = query(pos + 1, r, sum[idx]);
            q.emplace(ret, pos + 1, r, nxt, idx);
        }
    }
    cout << ans;
    return 0;
}

P9169 [省选联考 2023] 过河卒

非凡的题号就是要配上非凡的题目。
博弈图爆搜题。首先这个建图就是大模拟,记录三个棋子和谁先手,好多情况。
考虑拓扑排序进行转移,从胜负结果确定的地方倒推。考虑怎么处理环,并不是所有环上的点都是平局。假设当前位置 \(v\)\(B\) 必胜,那么如果它能倒推到一个 \(B\) 先手的状态 \(u\),那么这个状态就也是 \(B\) 必胜,就算他在环上也可以直接从它那里把环断开(放进拓扑的队列里了)。由于 bfs 的性质,直接从第一个遍历到 \(u\)\(v\) 转移步数即可(一定最小)。对于 \(R\) 也同理。反之,如果一个点是正常拓扑排序入队的,就说明它所能到达的状态都是对手必胜,直接从解开它最后一条边的那个点(一定最大)转移步数。如果到最后都没有遍历到一个点,那就是真的平局了。
卡常,注意常数。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 15, M = 2e6 + 5, V = 2e6;
#define cvt(x, y) (m * (x - 1) + y - 1)
int cnt, head[M], q[M], pl, pr, n, m, f[M], step[M], way[4][2] = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}}, deg[M];
char mp[N][N];
bitset<M> bs;
struct edge{
	int v, pre;
}e[M * 5];
inline int getid(int x){
    return (x - 1) / 1000000;
}
inline int idx(int id, int a, int b, int c, int d, int e, int f){
    int ret = cvt(e, f) + cvt(c, d) * 100 + cvt(a, b) * 10000 + id * 1000000;
    bs[ret] = 1;
    return ret;
}
inline bool eq(int a, int b, int c, int d){
    return (a == c && b == d);
}
inline bool in(int a, int b){
    return a >= 1 && a <= n && b >= 1 && b <= m;
}
inline void adde(int u, int v){
    e[++cnt] = {u, head[v]};
	deg[u]++;
	head[v] = cnt;
}
inline void solve(){
	cnt = 0;
	memset(deg, 0, sizeof(deg));
    memset(head, 0, sizeof(head));
    memset(f, -1, sizeof(f));
    bs.reset();
    cin >> n >> m;
    int b[2], r[2][2], cnt = 0;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            cin >> mp[i][j];
            if(mp[i][j] == 'X'){
                b[0] = i, b[1] = j;
            }
            if(mp[i][j] == 'O'){
                r[cnt][0] = i, r[cnt][1] = j;
                ++cnt;
            }
        }
    }
    for(int id : {0, 1}){
        for(int j = 1; j <= m; ++j){
			if(mp[1][j] == '#') continue;
            for(int a = 1; a <= n; ++a){
                for(int b = 1; b <= m; ++b){
					if(mp[a][b] == '#') continue;
                    for(int c = 1; c <= n; ++c){
                        for(int d = 1; d <= m; ++d){
                            if(mp[c][d] == '#' || eq(a, b, c, d)) continue;
                            int x = idx(id, 1, j, a, b, c, d);
                            step[x] = 0;
                            f[x] = 0;
                        }
                    }
                }
            }
        }
    }
    for(int id : {0, 1}){
        for(int i = 2; i <= n; ++i){
            for(int j = 1; j <= m; ++j){
				if(mp[i][j] == '#') continue;
                for(int a = 1; a <= n; ++a){
                    for(int b = 1; b <= m; ++b){
                        if(mp[a][b] == '#' || eq(i, j, a, b)) continue;
                        int x = idx(id, i, j, i, j, a, b), y = idx(id, i, j, a, b, i, j);
                        step[x] = step[y] = 0;
                        f[x] = f[y] = id ^ 1;
                    }
                }
            }
        }
    }
    for(int i = 2; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
			if(mp[i][j] == '#') continue;
            for(int a = 1; a <= n; ++a){
                for(int b = 1; b <= m; ++b){
					if(mp[a][b] == '#') continue;
                    for(int c = 1; c <= n; ++c){
                        for(int d = 1; d <= m; ++d){
                            if(mp[c][d] == '#' || eq(a, b, c, d)) continue;
                            int x = idx(0, i, j, a, b, c, d);
                            if(f[x] != -1) continue;
                            for(int k = 0; k < 3; ++k){
                                int xx = i + way[k][0], yy = j + way[k][1];
                                if(in(xx, yy) && mp[xx][yy] != '#') adde(x, idx(1, xx, yy, a, b, c, d));
                            }
                        }
                    }
                }
            }
        }
    }
    for(int i = 2; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
			if(mp[i][j] == '#') continue;
            for(int a = 1; a <= n; ++a){
                for(int b = 1; b <= m; ++b){
					if(mp[a][b] == '#') continue;
                    for(int c = 1; c <= n; ++c){
                        for(int d = 1; d <= m; ++d){
                            if(mp[c][d] == '#' || eq(a, b, c, d)) continue;
                            int x = idx(1, i, j, a, b, c, d);
                            if(f[x] != -1) continue;
                            for(int k = 0; k < 4; ++k){
                                int xx = a + way[k][0], yy = b + way[k][1];
                                if(in(xx, yy) && mp[xx][yy] != '#' && !eq(xx, yy, c, d)) adde(x, idx(0, i, j, xx, yy, c, d));
                            }
                            for(int k = 0; k < 4; ++k){
                                int xx = c + way[k][0], yy = d + way[k][1];
                                if(in(xx, yy) && mp[xx][yy] != '#' && !eq(xx, yy, a, b)) adde(x, idx(0, i, j, a, b, xx, yy));
                            }
                        }
                    }
                }
            }
        }
    }
    pl = 1, pr = 0;
    for(int i = 1; i <= V; ++i){
        if(bs[i]){
            if(!deg[i]){
                if(f[i] == -1) f[i] = getid(i) ^ 1, step[i] = 0;
                q[++pr] = i;
            }
        }
    }
    int x = idx(1, b[0], b[1], r[0][0], r[0][1], r[1][0], r[1][1]);
    while(pl <= pr){
        int u = q[pl]; ++pl;
        int id = getid(u);
        if(f[u] == -1) f[u] = id ^ 1;
        if(u == x) return cout << (f[x] == 0 ? "Black" : "Red") << ' ' << step[x] << '\n', void();
        for(int k = head[u]; k; k = e[k].pre){
			int v = e[k].v;
            if(!deg[v]) continue;
            int x = getid(v);
            if(x == f[u]){
                deg[v] = 0;
                f[v] = x;
                step[v] = step[u] + 1;
                q[++pr] = v;
            }
            else{
                deg[v]--;
                if(!deg[v]){
                    step[v] = step[u] + 1;
                    q[++pr] = v;
                }
            }
        }
    }
    cout << "Tie\n";
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    int c, T;
    cin >> c >> T;
    while(T--) solve();
    return 0;
}

P5361 [SDOI2019] 热闹的聚会与尴尬的聚会

首先可以发现最大化 \(p\) 肯定是不劣的。考虑开一个优先队列,每次取出度数最小的点,然后更新与他相连接的点(减度数)。然后看如果当前节点的度数比当前答案 \(p\) 更大,就更新 \(p\),由于我们每次取得都是最小的,所以剩下的节点构成一个度数全都 \(\ge p\) 的子图。实现上可以开一个 vec,每次加入去出来的点,如果大于 \(p\) 就清空再加,这与上面那个方法是等价的。
如果考虑在求出 \(p\) 的过程中,同步求出一个合法的点独立集,我们发现可以在取出度数最小的点的时候顺便把相邻的点标记为不能选。如果一个点在取出的时候没有被标记,那么就把它选入点独立集。由于我们求出的是最大的 \(p\),也就是说每次最多删掉(自己和标记的)\(deg_u + 1\le p + 1\) 个,那这样就最多能选出 \(\lfloor \frac{n}{p + 1} \rfloor\) 的点,由于条件 1 和条件 2 等价,本题就构造完了。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 5;
using pii = pair<int, int>;
vector<int> a, b, e[N];
int deg[N];
bitset<N> done, vis;
void solve(){
    int n, m;
    cin >> n >> m;
    done.reset(), vis.reset();
    a.clear(), b.clear();
    memset(e, 0, sizeof(e));
    priority_queue<pii, vector<pii>, greater<pii> > q;
    for(int i = 1; i <= m; ++i){
        int u, v;
        cin >> u >> v;
        e[u].emplace_back(v);
        e[v].emplace_back(u);
    }
    for(int i = 1; i <= n; ++i){
        deg[i] = e[i].size();
        q.emplace(deg[i], i);
    }
    int mx = 0;
    while(!q.empty()){
        auto [d, u] = q.top();
        q.pop();
        if(done[u]) continue;
        done[u] = 1;
        if(d > mx) mx = d, a.clear();
        a.emplace_back(u);
        if(!vis[u]){
            b.emplace_back(u);
            for(int v : e[u]){
                if(done[v]) continue;
                vis[v] = 1;
            }
        }
        for(int v : e[u]){
            if(done[v]) continue;
            deg[v]--;
            q.emplace(deg[v], v);
        }
    }
    cout << a.size() << ' ';
    for(int u : a) cout << u << ' ';
    cout << '\n';
    cout << b.size() << ' ';
    for(int u : b) cout << u << ' ';
    cout << '\n';
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    int T;
    cin >> T;
    while(T--) solve();
    return 0;
}

P3228 [HNOI2013] 数列

显然可以背包,但是各个维度都太大了。
滚榜一样,由于题目限制了原序列单调不降,并且 \(N > M(K - 1)\),特判掉 \(K = 1\) 之后,我们可以考虑对差分数组进行计数。要算的是再乘上第一天的方案数 \(\sum\limits_{d_1 = 1}^M \sum\limits_{d_2 = 1}^M \cdots \sum\limits_{d_{K - 1} = 1}^M (N - d_1 - d_2 - \cdots - d_{K - 1})\)
然后是一些求和式处理的技巧,考虑贡献,原式就变成 \(N \times M^{K - 1} - M^{K - 2}(\sum\limits_{d_1 = 1}^M d_1 + \sum\limits_{d_2 = 1}^M d_2 + \cdots + \sum\limits_{d_{K - 1}}^M d_{K - 1})\),等差数列求和化简一下就行了,\(N \times M^{K - 1} - M^{K - 2}\times (K - 1) \times \frac{M \times(M+1)}{2}\)

Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll mod, n, m, k;
ll qpow(ll a, ll b){
    ll ret = 1;
    for(; b; b >>= 1, (a *= a) %= mod){
        if(b & 1) (ret *= a) %= mod;
    }
    return ret;
}
ll G(ll m){
    return (m * (m + 1) / 2) % mod;
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> k >> m >> mod;
    if(k == 1) return cout << n, 0; 
    n %= mod;
    ll ans = n * qpow(m, k - 1) % mod - qpow(m, k - 2) * (k - 1) % mod * G(m) % mod;
    cout << (ans % mod + mod) % mod;
    return 0;
}

P6622 [省选联考 2020 A/B 卷] 信号传递

首先显然有 \(O(N^22^N)\) 的状压 \(dp\),主要瓶颈是算这个转移代价 \(cost_{mask, i}\)。注意到,这个转移代价也是可以从前面转过来的。只需要 \(mask\) 枚举中为 1 任意一位 \(k\)(为了 O(1) 找到这一位当然是选择 lowbit),然后 \(cost_{mask/{k}, i}\) 减掉 \(k\) 不在 \(mask\) 中贡献加上 \(k\)\(mask\) 中的贡献即可。
压一下空间,按 \(popcount\) 分层转移就行。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5, M = 23, K = 1.36e6 + 5;
int n, m, k, s[N], pw[1 << M], cnt[M][M], f[1 << M], idx[1 << M], cost[2][K][M];
vector<int> sta[M];
int in(int j, int i){ return k * cnt[i][j] + cnt[j][i]; }
int notin(int j, int i){ return -cnt[i][j] + cnt[j][i] * k; }
int lowbit(int x){ return x & (-x); }
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> m >> k;
    for(int i = 1; i <= n; ++i){
        cin >> s[i];
        s[i]--;
        if(i > 1) cnt[s[i - 1]][s[i]]++;
    }
    for(int i = 0; i < m; ++i) pw[1 << i] = i;
    for(int i = 0; i < (1 << m); ++i){
        int num = __builtin_popcount(i);
        sta[num].push_back(i);
        idx[i] = sta[num].size();
    }
    int nw = 0;
    for(int i = 0; i < m; ++i){
        for(int j = 0; j < m; ++j){
            if(i != j) cost[nw][idx[0]][i] += notin(j, i);
        }
    }
    memset(f, 0x3f, sizeof(f));
    f[0] = 0;
    for(int s = 1; s <= m; ++s){
        nw ^= 1;
        memset(cost[nw], 0, sizeof(cost[nw]));
        for(int k : sta[s]){
            for(int i = 0; i < m; ++i){
                if(k & (1 << i)) continue;
                int x = lowbit(k), y = pw[x];
                cost[nw][idx[k]][i] = cost[nw ^ 1][idx[k ^ x]][i] - notin(y, i) + in(y, i);
            }
        }
        for(int k : sta[s]){
            for(int i = 0; i < m; ++i){
                if(k & (1 << i)){
                    int pre = k ^ (1 << i);
                    f[k] = min(f[k], f[pre] + s * cost[nw ^ 1][idx[pre]][i]);
                }
            }
        }
    }
    cout << f[(1 << m) - 1];
    return 0;
}

P4364 [九省联考 2018] IIIDX

把树建出来之后,就是要求父亲的权值比儿子大。按位考虑,\(d_{1}\) 能取到的最大的 \(k\) 是要保证有 \(\ge k\) 的数字数量要 \(\ge siz_{1}\)。找到 \(k\) 的最大值之后,发现这是对后面有限制的,也就是要求后面在取最大的时候,要始终留够 \(siz_1\)\(\ge k\) 的数。反过来说,选出 \(k\) 之后,我们就要添加一条限制 \((k, siz_1)\),表示接下来要在 \(\ge k\) 的被拿走了 \(siz_1\) 个的情况下去找字典序最大的。
如果只有添加操作,那就线段树上模拟然后二分就行。但问题是,遍历到 \(1\) 的儿子时,它的限制会被解除。也就是说我们还得支持限制的删除操作。也就是说,我们要维护一个数据结构。可以做到添加一条限制 \((a, b)\) 表示要拿走 \(\ge a\) 的数中的 \(b\) 个,删除限制,查询最大能取到的值。
接下来几步不太自然,设 \(f_i\) 表示只考虑 \(a \ge i\) 的限制时,\(\ge i\) 的数还剩多少个。结论是,最终 \(\ge k\) 的个数是 \(\min_{i \le k} f_i\)。按道理来说,\(f_i\) 应该是单调递减的,因为 \(\ge i\) 的个数本来就在减少,但是现在如果前面的一个 \(f_i\)\(f_k\) 还小,说明有限制已经占用到 \(\ge k\) 的位置了。
一举两得的是,加上了 \(\min\) 之后 \(g_k = \min_{i \le k} f_i\) 具有了单调性。而且我们发现 \(f_i\) 是容易维护的,添加一条限制 \((a, b)\) 就是对 \([1, a]\) 区间 \(-b\),删除同理。注意到在选完一个数之后还有一个永久的删除 \(x\) 操作,这个也是对 \([1, x]\) 区间 \(-1\)。线段树上维护区间 \(f\) 的最小值,然后线段树二分找到第一个前缀最小值 \(<siz_u\) 的,它的上一个就是能取到的最大值。

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int mn[N << 2], tag[N << 2], V, n, d[N], siz[N], idx[N], ans[N];
double k;
vector<int> e[N];
map<int, int> mp;
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
#define fa(p) ((int)floor(double(p) / k))
void addtag(int p, int k){ tag[p] += k, mn[p] += k; }
void pushup(int p){ mn[p] = min(mn[ls(p)], mn[rs(p)]); }
void pushdown(int p){ if(tag[p]){ addtag(ls(p), tag[p]), addtag(rs(p), tag[p]), tag[p] = 0; }} 
void add(int L, int R, int k, int p = 1, int pl = 1, int pr = V){
    if(L <= pl && R >= pr) return addtag(p, k);
    int mid = (pl + pr) >> 1;
    pushdown(p);
    if(L <= mid) add(L, R, k, ls(p), pl, mid);
    if(R > mid) add(L, R, k, rs(p), mid + 1, pr);
    pushup(p);
}
int find(int k, int p = 1, int pl = 1, int pr = V){
    if(pl == pr) return (mn[p] < k ? pl : pl + 1);
    int mid = (pl + pr) >> 1;
    pushdown(p);
    if(mn[ls(p)] < k) return find(k, ls(p), pl, mid);
    return find(k, rs(p), mid + 1, pr);
}
void dfs(int u){
    siz[u] = 1;
    for(int v : e[u]){
        dfs(v);
        siz[u] += siz[v];
    }
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> k;
    for(int i = 1; i <= n; ++i) cin >> d[i], mp[d[i]]++;
    V = mp.size();
    int cnt = 0;
    for(auto [x, y] : mp){
        idx[++cnt] = x;
        add(1, cnt, y);
    }    
    for(int i = 1; i <= n; ++i){
        e[fa(i)].emplace_back(i);
    }
    dfs(0);
    for(int i = 1; i <= n; ++i){
        if(fa(i)) add(1, ans[fa(i)], siz[i]);
        ans[i] = find(siz[i]) - 1; // 变成找最小的那个前缀 min 小于 k 的位置
        add(1, ans[i], -siz[i]);
        cout << idx[ans[i]] << ' ';
    }
    return 0;
}

P6279 [USACO20OPEN] Favorite Colors G

一开始居然没想清楚合并上限是多少。实际上主要你不合并重复的点,合并次数就是 \(N - 1\)
题意就是一直在合并。首先把一个点的出边合并到一起,然后还得把这些点的出边合并到一起。也就是说,我们既要维护连通块,也要维护连通块的出边。启发式合并维护这个就行。由于在点向上合并的时候,所连带的出边也向上合并,由于祖先的数量级是 \(O(\log N)\),每个点的出边也只会被遍历到 \(O(\log N)\) 次,不对出边去重,复杂度也是对的,因此复杂度 \(O((M + N) \log N)\)

Code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
vector<int> e[N], s[N];
int f[N], ans[N];
bitset<N> in;
queue<int> q;
void merge(int x, int y){
    x = f[x], y = f[y];
    if(x == y) return;
    if(s[x].size() > s[y].size()) swap(x, y);
    for(int v : s[x]){ f[v] = y; s[y].emplace_back(v); }
    e[y].insert(e[y].end(), e[x].begin(), e[x].end());
    e[x].clear(); s[x].clear();
    if(!in[y] && e[y].size() > 1) q.push(y), in[y] = 1;
} 
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= m; ++i){
        int u, v;
        cin >> u >> v;
        e[u].emplace_back(v);
    }
    for(int i = 1; i <= n; ++i){
        f[i] = i;
        s[i].emplace_back(i);
        if(e[i].size() > 1) q.push(i);
    }
    while(!q.empty()){
        int u = q.front(); q.pop(); in[u] = 0;
        for(int i = 1; i < e[u].size(); ++i) merge(e[u][0], e[u][i]);
    }
    int tot = 0;
    for(int i = 1; i <= n; ++i){
        int x = f[i];
        if(!ans[x]) ans[x] = ++tot;
    }
    for(int i = 1; i <= n; ++i) cout << ans[f[i]] << '\n';
    return 0;
}

P6521 [CEOI 2010] pin

考虑 \(f_T\) 表示最多 \(T\) 中的位置不同的对数,\(g_T\) 表示恰好 \(T\) 中的位置不同的对数,子集反演即可。\(f\) 的求法就是把 \(T\) 中的字符都赋值成相同的,然后开一个 \(map\) 记录每种本质不同的字符串有 \(y\) 个,以 \(y \choose 2\) 贡献到答案。

Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e4 + 5;
string s[N];
map<string, int> mp;
int n, g[16], f[16], d;
int get(int S){
    mp.clear();
    for(int i = 1; i <= n; ++i){
        string t = s[i];
        for(int j = 0; j < 4; ++j){
            if(S & (1 << j)) t[j] = '*';
        }
        mp[t]++;
    }
    int ans = 0;
    for(auto [x, y] : mp){
        ans += y * (y - 1) / 2;
    }
    return ans;
}
signed main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> d;
    for(int i = 1; i <= n; ++i) cin >> s[i];
    for(int i = 0; i < (1 << 4); ++i) f[i] = get(i);
    int ans = 0;
    for(int i = 0; i < (1 << 4); ++i){
        for(int j = i; ; j = i & (j - 1)){
            int tmp = __builtin_popcount(i ^ j);
            g[i] += (tmp & 1 ? -1 : 1) * f[j];
            if(j == 0) break;
        }
        int k = __builtin_popcount(i);
        if(k == d) ans += g[i];
    }
    cout << ans;
    return 0;
}

P10220 [省选联考 2024] 迷宫守卫

本题跟 IIIDX 有一点像。还是按位考虑,二分第一位的值 \(mid\),也就是无论如何不能让对手取到 \(\le mid\) 的值,求最小代价。叶子节点的初始值 $ = [q_u \le mid] \times inf$,转移就是 \(f_u \gets f_{ls(u)} + \min(w(u), f_{rs(u)})\)。最后判断 \(f_1\) 是否小于等于 \(K\)
接下来原问题被递归为 \(O(\log N)\) 子问题,要分别求解。但是子问题可用的魔力值不能简单地被赋值为 \(K' = K - f_1\)。我们从已经求出的 \(Q_1\) 所对应的那个叶子节点 \(u\) 出发一直往上跳到根,那么本次要递归进的就是 \(v = u \oplus 1\)。如果说 \(v\) 是左儿子,那么由于 \(f_1\) 中一定包含 \(f_v\) 的贡献,按 \(K' + f_v\) 递归进子问题(这隐含着相应的 \(v\) 的第一位也不可能 \(\le mid\))。如果 \(v\) 是右儿子,那么 \(f_1\) 中要么有 \(f_{v}\) 的贡献,要么有 \(w_{fa(v)}\) 的贡献。如果是从 \(f_{v}\) 转移过来的,类似左儿子把它直接加上就好。否则,我们判断 \(K' - w_{fa(v)} + f_{v}\) 是否还 \(\ge 0\),如果还是的话,这意味着我们可以给 \(K' + w\) 之后递归进子问题(这样我们可以在 \(Q_1\) 不被破坏的情况下是子问题取到更优)。否则,只能直接递归进去了(因为此时不按 \(w_{fa(v)}\) 转移是没办法把本轮中 \(\le mid\) 的那些点全部堵住的,无论如何不能破坏 \(Q_1\) 使得其更小)。

Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 5, inf = 1e18;
int f[N], w[N], n, ans[N], tot, k;
#define fa(p) (p >> 1)
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
#define isleaf(p) (p >= (1 << n))
void dfs(int u, int k){
    if(isleaf(u)) return f[u] = (w[u] < k ? inf : 0), void();
    dfs(ls(u), k), dfs(rs(u), k);
    f[u] = f[ls(u)] + min(f[rs(u)], w[u]);
}
void solve(int u, int dep){
    if(isleaf(u)) return ans[++tot] = w[u], void();
    vector<int> all;
    int d = n + 1 - dep;
    for(int i = (u << d); i <= (u << d) + (1 << d) - 1; ++i)
        all.emplace_back(w[i]);
    sort(all.begin(), all.end());
    int l = 0, r = all.size() - 1;
    while(l < r){
        int mid = (l + r + 1) >> 1;
        dfs(u, all[mid]);
        if(f[u] <= k)  l = mid;
        else r = mid - 1;
    }
    int ret = all[l], pos = 0, tmp = n + 1;
    ans[++tot] = ret;
    dfs(u, ret); k -= f[u];
    for(int i = (u << d); i <= (u << d) + (1 << d) - 1; ++i){
        if(w[i] == ret){ pos = i; break; }
    }
    for(int i = pos; i != u; i = fa(i)){
        if(i & 1) k += f[i ^ 1];
        else{
            if(f[fa(i)] == f[i] + f[i ^ 1]) k += f[i ^ 1];
            else if(f[i] + f[i ^ 1] - f[fa(i)] <= k) k += w[fa(i)];
        }
        solve(i ^ 1, tmp);
        --tmp;
    }
}
void solve(){
    cin >> n >> k;
    tot = 0;
    for(int i = 1; i < (1 << n + 1); ++i) cin >> w[i];
    solve(1, 1);
    for(int i = 1; i <= tot; ++i) cout << ans[i] << ' ';
    cout << '\n';
}
signed main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    int T;
    cin >> T;
    while(T--) solve();
    return 0;
}

P5203 [USACO19JAN] Exercise Route P

原题意可以转化,给定一颗树和一些路径,求互相有边相交的路径的对数(无序)。
先看点相交的弱化版,两条路径 \((u_1, v_1)\) 有交 \((u_2, v_2) \iff lca(u_1, v_1)\text{ is on }(u_2, v_2) \lor lca(u_2, v_2) \text{ is on } (u_1, v_1)\)。由于我们求的是无序的,那么就只需要遍历每条路径,看有多少个 \(lca\) 在这条路径上,加入答案即可。注意要把 \(lca\) 相同的那些路径的重复给去了。具体来说,假设有 \(k\) 条路径的 \(lca\) 都是同一个,那么就从答案中 \(-k^2 + {k \choose 2}\)
除了上面那个做法,还有点边容斥的做法。由于路径的交还是路径,满足点数 = 边数 + 1,那么对于一条路径,我们在它经过的点上面 +1,在边上面 -1。然后遍历每条路径,将这条路径上的点权和和边权和加起来贡献到答案,这样可以保证与他相交的路径恰好贡献一次。
二者最终都是用树上差分维护。
对于边相交的情况,把边下放到点之后就可以类似做了。当 \(L\)\(u\) 的祖先时,记录 \(f(u,L)\) 表示 \(u\) 是从 \(L\) 的哪个儿子 \(v\) 上来的。对于形如祖宗-儿子的路径 \((u,v)\),由于下放,要变成 \((f(v, u), v)\) 这条路径做点相交的做法。否则,则把一条路径 \(u \to v\) 拆成两条进行统计 \(f(u,lca) \to u\)\(f(v,lca) \to v\)。那么现在所有路径都变成了 \(u \to v\)\(u\)\(v\) 的祖先的形式。那我们遍历每条路径,看他上面有几个祖先节点贡献到答案即可。这样同起点的会算重一次,去重方法跟上面同 lca 的一样。但注意,拆链之后的答案并不与原问题等价,因为可能原本两条链拆完之后变成四条链在左右两边各自相交产生 2 的贡献,多算了一次。注意到这样两边都交的链只有可能是 \(f(u, lca)\)\(f(v,lca)\) 都相等,假设等价类中有 \(k\) 个,减一个 \(k \choose 2\) 即可。
点边容斥的做法也类似,也得按上面的办法去重。

Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 5;
using pii = pair<int, int>;
int n, m, dep[N], f[N][25], a[N], b[N], L[N], sum[N];
map<pii, int> buc;
vector<int> e[N];
void dfs(int u, int fa){
    dep[u] = dep[fa] + 1;
    f[u][0] = fa;
    for(int i = 1; (1 << i) <= dep[u]; ++i) f[u][i] = f[f[u][i - 1]][i - 1];
    for(int v : e[u]){
        if(v != fa){
            dfs(v, u);
        }
    }
}
int lca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);
    for(int i = 19; i >= 0; --i){
        if(dep[f[u][i]] >= dep[v])
            u = f[u][i];
    }
    if(u == v) return u;
    for(int i = 19; i >= 0; --i){
        if(f[u][i] != f[v][i]){
            u = f[u][i], v = f[v][i];
        }
    }
    return f[u][0];
}
int get(int u, int v){
    assert(u != v);
    for(int i = 19; i >= 0; --i){
        if(dep[f[u][i]] > dep[v]) u = f[u][i];
    }
    return u;
}
vector<int> split(int u, int v, int &Lca){
    vector<int> ret;
    if(dep[u] > dep[v]) swap(u, v);
    Lca = lca(u, v);
    assert(Lca > 0);
    if(u == Lca){
        int tp = get(v, u);
        ret.emplace_back(tp);
    }
    else{
        int tpu = get(u, Lca), tpv = get(v, Lca);
        assert(tpu != 1 && tpv != 1);
        ret.emplace_back(tpu), ret.emplace_back(tpv);
    }
    return ret;
} 
void ins(pii x){
    if(x.first > x.second) swap(x.first, x.second);
    buc[x]++;
}
int c2(int x){ return x * (x - 1) / 2; }
void getans(int u, int fa){
    sum[u] += sum[fa];
    for(int v : e[u]){
        if(v != fa) getans(v, u);
    }
}
signed main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> m;
    for(int i = 1; i < n; ++i){
        int u, v;
        cin >> u >> v;
        e[u].emplace_back(v);
        e[v].emplace_back(u);
    }
    dfs(1, 0);
    for(int i = n; i <= m; ++i){
        cin >> a[i] >> b[i];
        vector<int> rge = split(a[i], b[i], L[i]);
        for(int u : rge) sum[u]++;
        if(rge.size() == 2) ins({rge[0], rge[1]});
    }
    int ans = 0;
    for(int i = 2; i <= n; ++i) ans += -sum[i] * sum[i] + c2(sum[i]);
    for(auto [x, y] : buc) ans -= c2(y);
    getans(1, 0);
    for(int i = n; i <= m; ++i){
        ans += sum[a[i]] - sum[L[i]] + sum[b[i]] - sum[L[i]];
    }
    cout << ans;
    return 0;
}

P4363 [九省联考 2018] 一双木棋 chess

第一道轮廓线 dp。首先本题的已经被放了棋子的轮廓是成一个左上三角形的,才能用下面这种方法状压。
对于一个 \(n\)\(m\) 列的网格,沿着从右上角一直走到左下角会走 \(n + m\) 步,如果这一步是向下走的,那么就是一条竖着的轮廓线,记录为 \(1\),否则就是一条横着的轮廓线,记录为 \(0\)。把轮廓线向右或者向下推进一格,等价于把一个 \(01\) 变成 \(10\)
然后就是 min - max 博弈 \(dp\) 了,记录 \(f_{mask}\) 表示从 \(mask\) 到终点态的得分,记忆化搜索即可。

code
#include <bits/stdc++.h>
using namespace std;
const int N = 15, M = 20;
int a[N][N], b[N][N], f[1 << M], n, m, inf;
void chmax(int &x, int y){ x = max(x, y); }
void chmin(int &x, int y){ x = min(x, y); }
int dfs(int mask, int op){
    if(f[mask] != inf) return f[mask];
    if(!op) f[mask] = -inf;
    int x = 0 , y = m;
    for(int i = n + m - 1; i >= 1; --i){
        if(mask & (1 << i)) ++x; else --y;
        if(!(mask & (1 << i)) && (mask & (1 << i - 1))){
            int nxt = (mask ^ (1 << i)) ^ (1 << i - 1);
            if(!op) chmax(f[mask], dfs(nxt, op ^ 1) + a[x][y]);
            else chmin(f[mask], dfs(nxt, op ^ 1) - b[x][y]);
        }
    }
    return f[mask];
}
int main(){
    cin.tie(nullptr)->sync_with_stdio(0);
    cin >> n >> m;
    for(int i = 0; i < n; ++i){
        for(int j = 0; j < m; ++j) 
            cin >> a[i][j];
    }
    for(int i = 0; i < n; ++i){
        for(int j = 0; j < m; ++j)
            cin >> b[i][j];
    }
    memset(f, 0x3f, sizeof(f));
    inf = f[0];
    f[((1 << n) - 1) << m] = 0;
    cout << dfs((1 << n) - 1, 0);
    return 0;
}
posted @ 2025-10-08 18:58  Hengsber  阅读(35)  评论(0)    收藏  举报