1. 程式人生 > 實用技巧 >Codeforces 思維題彙總 2020(下篇)

Codeforces 思維題彙總 2020(下篇)

作為 \(2020\) 年的最後一篇博文,在今年 Codeforces 所有對積分 \(\ge 2100\) 以上 Rated 的比賽中,挑選了有代表性的 \(20\) 道思維題在這裡分享。

以下列舉的題目為後 \(10\) 題(\(7\) 月到 \(12\) 月),順序為題目編號順序。

1375F

題意

有三堆石子,數量分別為 \(a,b,c\)

每次先手可以選擇一個正整數 \(x\),後手選擇一堆石子使其數量加 \(x\),在任意相鄰的兩輪中,後手不能選擇同一堆石子。

如果後手操作之後有兩堆石子個數相同,則先手獲勝。如果 \(1000\) 輪內沒有出現這種情況,則後手勝。

判斷先手能否獲勝,如果能則給出策略。

\(1\le a,b,c\le10^9\)\(a,b,c\) 互不相同。

Solution

考慮什麼情況下離勝利只有一步:假設 \(a<b<c\)\(a,b,c\) 成等差數列,且上回操作加在了 \(c\) 上。令 \(x=b-a=c-b\) 就可以勝利。

我們的策略是:找一個合適的 \(x\),使得 \(x\) 加在其中兩堆上都能達到以上的狀態。

\(x=2c-a-b\),可以發現只要對面不把 \(x\) 加在 \(c\) 上,就能達到目的。

而如果對面這次把 \(x\) 加在了 \(c\) 上,下一次就無法加在 \(c\) 上了,仍然取 \(x=2c-a-b\) 即可把局面引導到離勝利只有一部的狀態。

先手用以上策略就能必勝。

Code

#include <bits/stdc++.h>

typedef long long ll;

ll a[4], b[4];

void orz() {b[1] = a[1]; b[2] = a[2]; b[3] = a[3]; std::sort(b + 1, b + 4);}

int main()
{
	int x;
	std::cin >> a[1] >> a[2] >> a[3];
	puts("First"); fflush(stdout); orz();
	do printf("%lld\n", b[3] * 2 - b[1] - b[2]), fflush(stdout),
		scanf("%d", &x), a[x] += b[3] * 2 - b[1] - b[2];
			while (orz(), b[1] + b[3] != b[2] * 2);
	printf("%lld\n", (orz(), b[2] - b[1])); fflush(stdout);
	if (scanf("%d", &x), !x) return 0;
	return 0;
}

1383E

題意

給定一個 \(01\)\(s\),每次可以找一個 \(1\le i<|s|\),把 \(s_i\)\(s_{i+1}\) 合併成一個,合併後的字元為 \(\max(s_i,s_{i+1})\),合併後新字元和左右兩側的子串會連起來。

求任意次操作(可以是零次)之後,能得到多少種不同的 \(01\) 串,對 \(10^9+7\) 取模。

Solution

這是一道典型的 AtCoder 風格計數題。

最終得到的 \(01\) 串可以視為把原串劃分成若干個子串,第 \(i\) 個子串中有 \(1\) 則最終串的第 \(i\) 位為 \(1\),全 \(0\) 則最終串第 \(i\) 位為 \(0\)

首先末尾的 \(0\) 會帶來一些困擾,先把末尾的 \(0\) 全部去掉,算出來的答案乘上(原末尾 \(0\) 的個數加 \(1\))即為最終答案。特判全 \(0\) 的情況。

這樣對於一個特定的最終 \(01\)\(t\)(最後一個數一定為 \(1\)),我們有一個貪心法則:

維護一個 \(j\) 初始為 \(1\),讓 \(i\) 從小到大:

(1)如果當前 \(t_i=1\) 就在 \(j\) 的後面找到第一個 \(1\) 設其位置為 \(k\),把 \([j,k]\) 作為第 \(i\) 段,令 \(j\leftarrow k+1\)\(i\leftarrow i+1\)

(2)如果當前 \(t_{i\dots x}=0\)\(t_{x+1}=1\),則在 \(j\) 的後面找到第一個長度為 \(x-i+1\) 的全 \(0\) 段,設其末尾為 \(k\),則把這 \(x-i+1\)\(0\) 分配給第 \(i\) 段到第 \(x\) 段,令 \(j\leftarrow k+1\)\(i\leftarrow x+1\)

這樣我們有了一個 DP:\(f_i\) 表示當前到了 \(s_i\) 的方案數。

第一種轉移(\(t\) 下一個字元為 \(1\)):如果 \(i\) 之後第一個 \(1\) 出現在 \(j\) 位置,則 \(f_j+=f_i\)

第二種轉移(\(t\) 後面有一段 \(x\)\(0\)):如果 \(i\) 之後第一個 \(0^x\) 的末尾出現在 \(j\) 位置,則 \(f_j+=f_i\)

值得注意的是 \(f_0\) 進行第二種轉移的時候 \(x\) 有上限(出現在 \(s\) 最前面的 \(0\) 個數),且只有 \(s_i=1\) 的時候才能用第二種轉移。

這樣暴力轉移是 \(O(n^2)\) 的,考慮第二種轉移的合法條件:設 \(c_i\) 表示以 \(i\) 結尾的最後一段 \(0\) 個數,則除了第一段零特殊處理之外,\(f_j+=f_i\) 需要滿足的條件是 \(\min_{i<k<j}c_k<c_j\)

顯然對於特定的 \(j\),能轉移的 \(i\) 是一段區間,我們可以得到區間的左端點 \(r\) 是滿足 \(r-1<j\)\(c_{r-1}\ge c_j\) 的最大 \(r\),用單調棧預處理出來之後,字首和優化即可,\(O(n)\)

Code

#include <bits/stdc++.h>

const int N = 1e6 + 5, djq = 1e9 + 7;

int n, a[N], f[N], ans, nxt[N], top, stk[N], pre[N], sum[N], endc;
char s[N];

int main()
{
	scanf("%s", s + 1); n = strlen(s + 1);
	int lst = n + 1;
	while (s[n - endc] == '0') endc++;
	if (endc == n) return std::cout << n << std::endl, 0;
	for (int i = 1; i <= n; i++) a[i] = s[i] == '1' ? 0 : a[i - 1] + 1;
	for (int i = n; i >= 0; i--)
		if (nxt[i] = lst, s[i] == '1') lst = i;
	f[0] = sum[0] = 1; stk[top = 0] = -1;
	for (int i = 1; i <= n; i++)
	{
		while (top && a[stk[top]] < a[i]) top--;
		pre[i] = stk[top]; stk[++top] = i;
	}
	for (int i = 0; i <= n; i++)
	{
		if (i) sum[i] = sum[i - 1];
		if (s[i] == '0')
		{
			f[i] = sum[i];
			if (pre[i] >= 0) f[i] = (f[i] - sum[pre[i]] + djq) % djq;
			else if (a[i] < i) f[i] = (f[i] + djq - 1) % djq;
		}
		else if (i) sum[i] = (sum[i] + f[i]) % djq;
		f[nxt[i]] = (f[nxt[i]] + f[i]) % djq;
	}
	for (int i = 1; i <= n; i++) if (s[i] == '1')
		ans = (1ll * (endc + 1) * f[i] + ans) % djq;
	return std::cout << ans << std::endl, 0;
}

1404D

題意

給定 \(n\),求是否存在一種方案把 \(1\)\(2n\) 的所有數劃分成 \(n\) 個二元組,滿足你無法在每個二元組中選出一個數,使得選出的所有 \(n\) 個數之和是 \(2n\) 的倍數。

如果是,則需要給出一種滿足條件的劃分方案;

如果否,則需要對於給定的一種劃分方案,在每個二元組中選出一個數,使得選出的所有 \(n\) 個數之和是 \(2n\) 的倍數。

\(1\le n\le5\times10^5\)

Solution

考慮這個構造:\((1,n+1)(2,n+2),\dots,(n,2n)\)

容易證明選出的所有 \(n\) 個數之和必然在模 \(n\) 意義下與 \(\frac{n\times(n+1)}2\) 同餘,當 \(n\) 為偶數時 \(\frac{n\times(n+1)}2\) 不是 \(n\) 的倍數,這樣以任意方式選出的 \(n\) 個數之和都不可能是 \(n\) 的倍數。

\(n\) 為奇數時,由於所有數之和模 \(2n\) 等於 \(n\),故我們只需找一個和是 \(n\) 的倍數的方案(如果這種方案下模 \(2n\) 等於 \(n\) 選補集即可)。

考慮建圖:對於一個二元組 \((x,y)\),連邊 \((x\bmod n,y\bmod n)\)

這樣建出來的圖每個點度數都為 \(2\),構成若干個環。將每個環定向之後,選取每條邊終點指向的數,這樣就對於每個 \(0\le i<n\),都取走了一個 \(\bmod n=i\) 的數,這 \(n\) 個數之和在模 \(n\) 意義下與 \(\frac{n\times(n+1)}2\) 同餘,當 \(n\) 為奇數時 \(\frac{n\times(n+1)}2\)\(n\) 的倍數,這樣選出的 \(n\) 個數之和是 \(n\) 的倍數。

複雜度 \(O(n)\)

Code

#include <bits/stdc++.h>

const int N = 1e6 + 5;

int n, p[N], ecnt = 1, nxt[N], adj[N], go[N], val[N];
bool vis[N], is[N], mark[N];
std::vector<int> a[N];

void add_edge(int u, int v, int w)
{
	nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; val[ecnt] = w;
}

int pos(int x) {return x > n ? x - n : x;}

int main()
{
	int x;
	std::cin >> n;
	if (!(n & 1))
	{
		puts("First"); fflush(stdout);
		for (int i = 1; i <= n; i++) printf("%d ", i);
		for (int i = 1; i <= n; i++) printf("%d ", i);
		puts(""); fflush(stdout);
		scanf("%d", &x); return 0;
	}
	puts("Second"); fflush(stdout);
	for (int i = 1; i <= (n << 1); i++) scanf("%d", &p[i]);
	for (int i = 1; i <= (n << 1); i++) a[p[i]].push_back(i);
	for (int i = 1; i <= n; i++)
		add_edge(pos(a[i][0]), pos(a[i][1]), a[i][1]),
		add_edge(pos(a[i][1]), pos(a[i][0]), a[i][0]);
	for (int u = 1; u <= n; u++) if (!vis[u])
		for (int v = u; !vis[v];)
		{
			int w;
			for (int e = adj[v]; e; e = nxt[e])
				if (!mark[e]) {mark[e] = mark[e ^ 1] = 1; w = go[e];
					is[val[e]] = 1; break;}
			vis[v] = 1; v = w;
		}
	int sum = 0;
	for (int i = 1; i <= (n << 1); i++) if (is[i])
		sum = (sum + i) % (n << 1);
	for (int i = 1; i <= (n << 1); i++)
		if ((sum > 0) ^ is[i]) printf("%d ", i);
	return puts(""), fflush(stdout), scanf("%d", &x), 0;
}

1427D

題意

一開始一個數集內只有一個奇數 \(x\),你需要運用加和異或兩種運算(運算元為當前集合內的數,可以重複使用),使得最後集合內出現 \(1\)

\(3\le x\le 999999\),操作次數上限為 \(10^5\),運算元需要在 \([0,5\times10^{18}]\) 內。

Solution

一個基本思路:每次都讓 \(x\) 的位數變小。

\(x\) 最高位的位權為 \(2^c\),則考慮 \(2^c\times x+x\) 這個數,可以發現這個數是由 \(x+1\)\(x-2^c\) 拼接成的

把這個數異或上 \(2^c\times x\) 再異或上 \(x\),得到的數就是 \(((x\oplus x+1)-1)\times2^c\)

注意到如果 \(x\) 末尾有 \(k\)\(1\),則 \(x\oplus x+1=2^{k+1}-1\),所以上式的值為 \(cur=(2^k-1)\times 2^{c+1}\)

然後考慮讓 \(cur\) 異或上形如 \(2^i\times x\) 的若干個數,使得 \(cur\) 小於 \(2^c\)

從高到低位處理,如果當前位權為 \(2^i\) 的位為 \(1\) 就異或上 \(2^{i-c}\times x\),這樣到最後就能使得 \(cur<2^c\)

如果這樣得到的 \(cur\) 為奇數則直接 \(x\leftarrow cur\),否則把 \(cur\) 一直乘 \(2\) 直到和 \(x\) 擁有共同最高位後再讓 \(x\) 異或上 \(cur\),就實現了去掉最高位。

上面忽略了一種特殊情況:如果 \(x=2^{c+1}-1\),則這時候得出來的 \(cur=0\),不能用以上方法處理。

這時需要特殊判斷:如果這種情況出現,則我們可以得到 \(2x\oplus4x=2^{c+2}+2\)\((2x\oplus x)+x=2^{c+2}\),異或一下可以得到 \(2\),讓 \(x\) 異或上 \(2\) 之後就避開了特殊情況,可以繼續使用以上方法。

Code

#include <bits/stdc++.h>

typedef long long ll;

const int N = 1e5 + 5;

int a, q;
ll x[N], y[N];
char op[N];

void ADD(ll u, ll v) {x[++q] = u; y[q] = v; op[q] = '+';}

void XOR(ll u, ll v) {x[++q] = u; y[q] = v; op[q] = '^';}

void reduce()
{
	int tmp = a, cnt = 0, fin;
	while (tmp > 1) tmp >>= 1, cnt++;
	if (a == (1 << cnt + 1) - 1)
	{
		ADD(a, a); XOR(a << 1, a); ADD((a << 1) ^ a, a);
		ADD(a << 1, a << 1); XOR(a << 2, a << 1);
		XOR(1 << cnt + 2, (1 << cnt + 2) | 2); XOR(a, 2); a ^= 2;
		if (a == 1) return;
	}
	for (int i = 0; i <= cnt; i++) ADD(1ll * a << i, 1ll * a << i);
	ADD(1ll * a << cnt, a); XOR((1ll * a << cnt) + a, 1ll * a << cnt);
	ll now;
	XOR(now = (1ll * a << cnt) + a ^ (1ll * a << cnt), a); now ^= a;
	for (int i = cnt + 1; (now >> i) & 1; i++) fin = i;
	for (int i = fin; i >= cnt; i--)
		if ((now >> i) & 1) XOR(now, 1ll * a << i - cnt),
			now ^= 1ll * a << i - cnt;
	if (now & 1) return (void) (a = now);
	while ((a ^ now) >= (1ll << cnt)) ADD(now, now), now <<= 1;
	XOR(now, a); a ^= now;
}

int main()
{
	std::cin >> a;
	while (a > 1) reduce();
	std::cout << q << std::endl;
	for (int i = 1; i <= q; i++)
		printf("%lld %c %lld\n", x[i], op[i], y[i]);
	return 0;
}

1442D

題意

\(n\) 堆物品,第 \(i\) 堆物品有 \(t_i\) 個,價值分別為 \(a_{i,1},a_{i,2},\dots,a_{i,t_i}\),滿足 \(0\le a_{i,1}\le a_{i,2}\le\dots\le a_{i,t_i}\)

對於每堆物品,你可以拿走前面的若干個物品,求拿走 \(k\) 個物品的最大總價值。

\(1\le n,k\le3000\)\(1\le t_i\le10^6\)\(k\le\sum_{i=1}^nt_i\le10^6\)\(0\le a_{i,j}\le10^8\)

Solution

結論:最多存在一個 \(i\) 滿足第 \(i\) 堆取走的物品數不是 \(0\) 也不是 \(t_i\)

證明:

設有兩堆,這兩堆拿走的最後一個物品價值分別為 \(x\)\(y\),第一堆的下一個物品價值為 \(u\),第二堆的下一個物品價值為 \(v\)
則第一堆物品多選一個,第二堆物品少選一個的價值增量為 \(u-y\),第二堆物品多選一個,第一堆物品少選一個的價值增量為 \(v-x\),這時需要滿足 \(u<y,v<x\)
\(x\le u,y\le v\) 合併起來可以得到 \(x\le u<y\le v<x\)\(x<x\),這是矛盾的。

於是我們有一個暴力:列舉一堆 \(i\),剩下的 \(n-1\) 堆都是整堆取或者整堆不取,可以直接做一個 \(O(nk)\) 的揹包 DP,設 DP 陣列為 \(f\),然後就可以列舉這一堆選的個數 \(x\)\(\sum_{j=1}^xa_{i,j}+f_{k-x}\) 更新答案了。

這樣的複雜度是 \(O(n^2k)\),考慮如何避免每次做一遍揹包。

可以分治:\(solve(l,r)\) 表示求出對於所有 \(l\le i\le r\),去掉第 \(i\) 堆的揹包陣列。

分治過程即取 \(mid=\lfloor\frac{l+r}2\rfloor\),遞迴到 \([l,mid]\) 前把第 \([mid+1,r]\) 堆全部加入,遞迴到 \([mid+1,r]\) 前把第 \([l,mid]\) 前全部加入。

這樣複雜度是 \(O(nk\log n)\)

Code

    #include <bits/stdc++.h>
     
    template <class T>
    inline void read(T &res)
    {
    	res = 0; bool bo = 0; char c;
    	while (((c = getchar()) < '0' || c > '9') && c != '-');
    	if (c == '-') bo = 1; else res = c - 48;
    	while ((c = getchar()) >= '0' && c <= '9')
    		res = (res << 3) + (res << 1) + (c - 48);
    	if (bo) res = ~res + 1;
    }
     
    template <class T>
    inline T Max(const T &a, const T &b) {return a > b ? a : b;}
     
    typedef long long ll;
     
    const int N = 3005, E = 16;
     
    int n, k, t[N], top;
    ll s[N], stk[E][N], f[N][N], ans;
    std::vector<ll> a[N];
     
    void xyz32768(int l, int r)
    {
    	if (l == r)
    	{
    		for (int i = 0; i <= k; i++) f[l][i] = stk[top][i];
    		return;
    	}
    	int mid = l + r >> 1;
    	top++;
    	for (int i = 0; i <= k; i++) stk[top][i] = stk[top - 1][i];
    	for (int i = mid + 1; i <= r; i++)
    		for (int j = k; j >= t[i]; j--)
    			stk[top][j] = Max(stk[top][j], stk[top][j - t[i]] + s[i]);
    	xyz32768(l, mid); top--;
    	top++;
    	for (int i = 0; i <= k; i++) stk[top][i] = stk[top - 1][i];
    	for (int i = l; i <= mid; i++)
    		for (int j = k; j >= t[i]; j--)
    			stk[top][j] = Max(stk[top][j], stk[top][j - t[i]] + s[i]);
    	xyz32768(mid + 1, r); top--;
    }
     
    int main()
    {
    	int x;
    	read(n); read(k);
    	for (int i = 1; i <= n; i++)
    	{
    		read(t[i]); a[i].push_back(0);
    		for (int j = 1; j <= t[i]; j++) read(x), a[i].push_back(x);
    		for (int j = 1; j <= t[i]; j++) a[i][j] += a[i][j - 1];
    		s[i] = a[i][t[i]];
    	}
    	xyz32768(1, n);
    	for (int i = 1; i <= n; i++)
    		for (int j = 0; j <= k && j <= t[i]; j++)
    			ans = Max(ans, f[i][k - j] + a[i][j]);
    	return std::cout << ans << std::endl, 0;
    }

1442E

題意

給定一棵樹,點有黑白灰三種,每次操作可以選擇一個連通塊,在這個連通塊內選一個點子集,把這些點和相連的邊刪掉,這個點子集不能同時包含黑點和白點。

求刪完這棵數的最小操作次數。

設樹的點數為 \(n\)\(1\le n\le2\times10^5\),多測,資料組數不超過 \(10^5\),所有資料的 \(n\) 之和不超過 \(2\times10^5\)

Solution

結論:如果只有黑點和白點,且顏色段數最多的一條鏈段數為 \(l\),則答案為 \(\lfloor\frac l2\rfloor+1\)

證明:

當樹為鏈時每次刪掉首尾兩段(若 \(l\) 為偶數則一開始先刪一段)即可達到 \(\lfloor\frac l2\rfloor+1\)
顯然地我們可以得到答案的下界是 \(\lfloor\frac l2\rfloor+1\)
考慮證明這個下界能取到:把同色點縮成一個,這樣得到的樹滿足每條邊連線的兩個點異色,在直徑上考慮:和之前刪掉首尾兩段一樣,每次刪掉所有跟直徑有關的葉子,就能讓直徑長度減 \(2\)

回到原問題,可以轉化成把未染色的點(灰點)染成黑或白,使得顏色段數最多的鏈段數最少。

先二分段數不超過 \(mid\),DP \(f_{u,0/1}\) 表示 \(u\) 的顏色為白或黑,\(u\) 的子樹最大段數不超過 \(mid\) 的前提下,以 \(u\) 出發的最小段數。

轉移直接從每個子樹的最小段數合併,根據 \(u\) 和子節點顏色的異同計算貢獻即可,算完之後如果某個 DP 值的最長鏈超過了 \(mid\) 就將其設為無效值。

複雜度 \(O(n\log n)\)

Code

    #include <bits/stdc++.h>
     
    template <class T>
    inline void read(T &res)
    {
    	res = 0; bool bo = 0; char c;
    	while (((c = getchar()) < '0' || c > '9') && c != '-');
    	if (c == '-') bo = 1; else res = c - 48;
    	while ((c = getchar()) >= '0' && c <= '9')
    		res = (res << 3) + (res << 1) + (c - 48);
    	if (bo) res = ~res + 1;
    }
     
    template <class T>
    inline T Min(const T &a, const T &b) {return a < b ? a : b;}
     
    const int N = 2e5 + 5, M = N << 1, INF = 0x3f3f3f3f;
     
    int n, a[N], ecnt, nxt[M], adj[N], go[M], f[N][2];
     
    void add_edge(int u, int v)
    {
    	nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
    	nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u;
    }
     
    void dfs(int u, int fu, int mid)
    {
    	f[u][0] = f[u][1] = INF;
    	if (a[u]) f[u][a[u] - 1] = 0; else f[u][0] = f[u][1] = 0;
    	int c0 = 0, c1 = 0;
    	for (int e = adj[u], v; e; e = nxt[e])
    		if ((v = go[e]) != fu)
    		{
    			dfs(v, u, mid);
    			if (a[u] != 2)
    			{
    				int t = Min(f[v][0], f[v][1] + 1);
    				if (t > f[u][0]) c0 = f[u][0], f[u][0] = t;
    				else if (t > c0) c0 = t;
    			}
    			if (a[u] != 1)
    			{
    				int t = Min(f[v][0] + 1, f[v][1]);
    				if (t > f[u][1]) c1 = f[u][1], f[u][1] = t;
    				else if (t > c1) c1 = t;
    			}
    		}
    	if (a[u] != 2 && f[u][0] + c0 > mid) f[u][0] = INF;
    	if (a[u] != 1 && f[u][1] + c1 > mid) f[u][1] = INF;
    }
     
    void work()
    {
    	int x, y;
    	read(n); ecnt = 0;
    	for (int i = 1; i <= n; i++) read(a[i]), adj[i] = 0;
    	for (int i = 1; i < n; i++) read(x), read(y), add_edge(x, y);
    	int l = 0, r = n;
    	while (l <= r)
    	{
    		int mid = l + r >> 1;
    		if (dfs(1, 0, mid), Min(f[1][0], f[1][1]) < INF) r = mid - 1;
    		else l = mid + 1;
    	}
    	std::cout << (l + 3 >> 1) << std::endl;
    }
     
    int main()
    {
    	int T; read(T);
    	while (T--) work();
    	return 0;
    }

1444D

題意

給定 \(h\) 條橫線和 \(v\) 條豎線,第 \(i\) 條橫線長為 \(l_i\),第 \(i\) 條豎線長為 \(p_i\)

要用這 \(h+v\) 條線在平面直角座標系上依次連成一個多邊形,需要滿足這個多邊形橫豎邊交替,且不能自交(在端點處重合不算自交)。

判斷是否存在方案,如果存在則構造一個。

\(1\le h,v,l_i,p_i\le1000\),多測,資料組數不超過 \(200\),所有資料的 \(h+v\) 之和不超過 \(1000\)

Solution

考慮為這個多邊形每條邊定向,使得所有邊以逆時針順次連線,橫線可以分成向右或向左,豎線可以分成向上和向下。

有解的條件比較好猜:有解當且僅當 \(h=v\),且 \(l\) 陣列和 \(p\) 陣列都能被分成兩個和相等的集合。

可以用 bitset 優化揹包 DP 來實現把 \(l\) 陣列和 \(p\) 陣列分成兩個集合的操作。

考慮如何構造,先從一種特殊情況入手:\(l\) 陣列和 \(p\) 陣列分出的第一個集合大小相同。

\(l\) 分成的兩個集合分別為 \(L_1,L_2\)\(p\) 分成的兩個集合分別為 \(P_1,P_2\)\(X=\sum_{i\in L_1}l_i=\sum_{i\in L_2}l_i\)\(Y=\sum_{i\in P_1}p_i=\sum_{i\in P_2}p_i\),考慮先從 \((0,0)\) 利用 \(L_1\)\(P_1\) 內的向量到達 \((X,Y)\),再利用 \(L_2,P_2\) 內的向量回到原點。

重點在於如何做到不自交。先從 \((0,0)\)\((X,Y)\),考慮這樣一個構造:

\(L_1\) 從長到短排序,\(P_1\) 從短到長排序,然後依次 \(i\) 從小到大,交替使用 \(L_1\) 排序後的第 \(i\) 個向量(向右)和 \(P_1\) 排序後的第 \(i\) 個向量(向上)。

我們可以證明這麼走的範圍一定會限制在 \((0,0)(X,Y)\) 連線的右下側:

考慮歸納,設 \(L_1\) 中最長的線段為 \(a\)\(P_1\) 中最短的線段為 \(b\),則不難發現點 \((a,b)\) 必然在連線的右下側,可以從 \((0,0)\) 走到 \((a,b)\)。而對於 \((a,b)\)\((X,Y)\) 的過程是個規模小 \(1\) 的子問題,可以歸納下去。

\((X,Y)\)\((0,0)\) 同理(只是變成了交替往左和往下走),不難發現兩段路徑必然第一段在 \((0,0)(X,Y)\) 連線的右下側,第二段在連線的左上側,不會發生自交。

如果 \(|L_1|\ne|P_1|\) 怎麼辦?考慮假設 \(|L_1|<|P_1|\)(如果不滿足,交換 \(L_1,L_2\)\(P_1,P_2\) 即可)。

考慮把這些向量分成三組,即路徑分成三段:

(1)右向量為 \(L_1\),上向量為 \(P_1\) 的前 \(|L_1|\) 個元素。

(2)左向量為 \(L_2\) 的前 \(|P_1|-|L_1|\) 個元素,上向量為 \(P_1\) 的後 \(|P_1-L_1|\) 個元素。

(3)左向量為 \(L_2\) 的後 \(|P_2|\) 個元素,下向量為 \(P_2\)

不難發現第一段路徑到達的點 \((x_1,y_1)\) 和第二段路徑到達的點 \((x_2,y_2)\) 滿足 \(x_2\le x_1\)\(y_2\ge y_1\),這時候對於第一段和第三段路徑使用前面的構造方法,第二段路徑任意(只需滿足橫豎邊交替即可),仍然可以保證不自交。

\(h=v=n\)\(\sum l+\sum p=m\),則單組資料總複雜度 \(O(\frac{nm}{\omega})\)

Code

#include <bits/stdc++.h>

template <class T>
inline void read(T &res)
{
	res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	if (bo) res = ~res + 1;
}

const int N = 1005, M = N * N / 2;

int n, a[N], b[N], pa[N], pb[N], dx[N], dy[N];
std::bitset<M> f[N];

bool part(int *a, int *res)
{
	f[0].reset(); f[0][0] = 1;
	int sum = 0;
	for (int i = 1; i <= n; i++) sum += a[i];
	if (sum & 1) return 0; sum >>= 1;
	for (int i = 1; i <= n; i++) f[i] = f[i - 1] | (f[i - 1] << a[i]);
	if (!f[n][sum]) return 0;
	for (int i = n; i >= 1; i--)
		if (f[i - 1][sum]) res[i] = -1;
		else res[i] = 1, sum -= a[i];
	return 1;
}

inline bool o(int x, int y) {return x > y;}

void work()
{
	int tn;
	read(tn);
	for (int i = 1; i <= tn; i++) read(a[i]);
	read(n);
	for (int i = 1; i <= n; i++) read(b[i]);
	if (tn != n) return (void) puts("No");
	if (!part(a, pa)) return (void) puts("No");
	if (!part(b, pb)) return (void) puts("No");
	int cnt1 = 0, cnt2 = 0;
	for (int i = 1; i <= n; i++) cnt1 += pa[i] > 0, cnt2 += pb[i] > 0;
	if (cnt1 > cnt2)
	{
		for (int i = 1; i <= n; i++)
			pa[i] = -pa[i], pb[i] = -pb[i];
		cnt1 = n - cnt1; cnt2 = n - cnt2;
	}
	std::vector<int> posa, nega, posb, negb;
	for (int i = 1; i <= n; i++)
		(pa[i] > 0 ? posa : nega).push_back(a[i]),
			(pb[i] > 0 ? posb : negb).push_back(b[i]);
	std::sort(posa.begin(), posa.end(), o);
	std::sort(posb.begin(), posb.end());
	std::sort(nega.begin(), nega.end(), o);
	std::sort(negb.begin(), negb.end());
	for (int i = 1; i <= cnt1; i++) dx[i] = posa[i - 1], dy[i] = posb[i - 1];
	for (int i = cnt1 + 1; i <= cnt2; i++)
		dx[i] = -nega[i - cnt1 - 1], dy[i] = posb[i - 1];
	for (int i = cnt2 + 1; i <= n; i++)
		dx[i] = -nega[i - cnt1 - 1], dy[i] = -negb[i - cnt2 - 1];
	puts("Yes");
	for (int i = 1, x = 0, y = 0; i <= n; i++)
		printf("%d %d\n", x, y), x += dx[i],
		printf("%d %d\n", x, y), y += dy[i];
}

int main()
{
	int T; read(T);
	while (T--) work();
	return 0;
}

1450G

題意

給定一個長度為 \(n\),字符集為 \(20\) 的字串,每次操作可以選擇兩個個字元 \(x\ne y\),滿足 \(x\)\(y\) 都在現在的串中出現過,且如果字元 \(x\) 出現的位置從小到大依次為 \(i_1,i_2,\dots,i_m\),則需要滿足 \(\frac ab\times (i_m-i_1+1)\le m\),把所有的字元 \(x\) 改成 \(y\)

求出所有的字元 \(x\),滿足可以通過若干次操作使得所有的字元都變成 \(c\)

\(1\le n\le 5000\)\(1\le a\le b\le 10^5\)

Solution

把一些字符合併成一個之後,這個字元可以用一個三元組 \((l,r,m)\) 表示,即出現的第一個和最後一個位置分別為 \(l\)\(r\),出現了 \(m\) 次。

結論:對於兩個字元 \((l_1,r_1,m_1)\)\((l_2,r_2,m_2)\),如果 \([l_1,r_1]\)\([l_2,r_2]\) 有交,且這兩個字元都滿足 \(\frac ab\times (r-l+1)\le m\),則這兩個字符合並後仍然滿足。
證明:在所給的條件下,合併之後 \(m\) 等於 \(m_1\)\(m_2\) 之和,\(r-l+1\) 小於 \(r_1-l_1+1\)\(r_2-l_2+1\) 之和,易證。

回到原問題,設 \(f_S\) 表示字元集合 \(S\) 能否被合併成一個,\(g_S\) 表示字元集合 \(S\) 能否被合併成若干個滿足 \(\frac ab\times (r-l+1)\le m\) 的字元。

不難發現把所有的字元都變成 \(c\),相當於把除 \(c\) 以外的所有字元都合併成若干個滿足 \(\frac ab\times (r-l+1)\le m\) 的字元之後再一一合併到 \(c\) 上。

所以 \(f_S=\bigcup_{c\in S}g_{S-c}\),最終答案所有字元都能變成 \(c\) 當且僅當 \(g_{\Sigma-c}=true\)

考慮 \(g\) 的轉移,由之前的性質得到,\(S\) 合併成的字元區間兩兩不交。

到這裡我們不難發現可以把 \(S\) 中的區間劃分成若干個連通塊,滿足屬於不同連通塊的區間不交,求出這些連通塊內部字元的區間並。

這時候可以注意到,如果把這些連通塊從左到右排列,那麼 \(S\) 中的字符合併成的每個字元一定對應了連續的幾個連通塊。

轉化成段的劃分問題,這時候就可以把原來的子集列舉改成分段 DP,\(O(|\Sigma|^2)\) 解決。

總複雜度 \(O(n+2^{|\Sigma|}|\Sigma|^2)\)

Code

#include <bits/stdc++.h>

template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}

template <class T>
inline T Max(const T &a, const T &b) {return a > b ? a : b;}

const int N = 5005, E = 22, C = (1 << 20) + 5;

int n, a, b, m, bel[300], l[E], r[E], sze[E], tot, o[E], st[E], cnt, w;
bool is[300], f[C], g[C], h[C], fl[E];
char s[N], ch[E];

int main()
{
	std::cin >> n >> a >> b;
	scanf("%s", s + 1);
	for (int i = 1; i <= n; i++) is[s[i]] = 1;
	for (char c = 'a'; c <= 'z'; c++) if (is[c]) ch[bel[c] = ++m] = c;
	for (int i = 1; i <= n; i++) r[bel[s[i]]] = i, sze[bel[s[i]]]++;
	for (int i = n; i >= 1; i--) l[bel[s[i]]] = i;
	f[0] = g[0] = 1;
	for (int S = 1; S < (1 << m); S++)
	{
		tot = w = 0;
		for (int i = 1; i <= m; i++)
			if ((S >> i - 1) & 1) o[++tot] = i,
				f[S] |= g[S ^ (1 << i - 1)];
		std::sort(o + 1, o + tot + 1, [&](int x, int y)
			{return r[x] < r[y];});
		int lt = 9973, rt = -9973, sum = 0;
		for (int i = tot, mn = 9973; i >= 1; i--)
		{
			if (r[o[i]] < mn) st[++w] = 1 << o[i] - 1;
			else st[w] |= 1 << o[i] - 1;
			mn = Min(mn, l[o[i]]);
			lt = Min(lt, l[o[i]]); rt = Max(rt, r[o[i]]);
			sum += sze[o[i]];
		}
		h[S] = a * (rt - lt + 1) <= sum * b; fl[0] = 1;
		for (int i = 1; i <= w; i++)
		{
			fl[i] = 0;
			for (int j = i, t = st[i]; j >= 1; j--, t |= st[j])
				if (fl[j - 1] && f[t] && h[t])
					{fl[i] = 1; break;}
		}
		g[S] = fl[w];
	}
	for (int c = 1; c <= m; c++) if (g[(1 << m) - 1 ^ (1 << c - 1)]) cnt++;
	std::cout << cnt << " ";
	for (int c = 1; c <= m; c++)
		if (g[(1 << m) - 1 ^ (1 << c - 1)])
			putchar(ch[c]), putchar(' ');
	return puts(""), 0;
}

1450H1 1450H2

題意

有一個長度為 \(n\)(偶數)的環,環上每個點可以為黑色或白色,黑色和白色的點個數都是偶數。

同色的點之間可以連邊,邊的顏色和這兩個點的顏色相同。你需要找到一組匹配,使得異色且相交的邊的對數儘可能少。

你有一個長度為 \(n\) 的字串 \(s\),包含 b(黑色)、w(白色),?(未知)來描述這個環,且至少有一個 ?。你需要求出在 ? 的顏色任意為黑色和白色(需要滿足黑色和白色的點數都為偶數)的情況下,異色且相交的邊的對數最小值的期望。

除此之外,有 \(m\) 次修改,每次會修改 \(s\) 的一個下標,然後需要再次回答。

\(2\le n\le2\times10^5\)\(0\le m\le2\times10^5\),對於簡單版 \(m=0\)

Solution

結論 \(1\):相鄰的兩個同色點直接消除掉一定是最優方案。
證明:設 \(i\) 原來連了 \(j\)\(i+1\) 原來連了 \(k\),那麼 \((i,i+1)\)\(j\)\(k\) 把環上剩下的點劃分成了三個部分,畫個圖討論一下可以發現無論另一條邊的起點和終點分別在哪個部分內,把 \((i,j)(i+1,k)\) 換成 \((i,i+1)(j,k)\) 都不會讓那條邊與 \(i,i+1,j,k\) 相交的次數變多,得證。

由這個結論可以推出,如果斷環為鏈,維護一個棧,依次把顏色加入棧中,判斷如果與棧頂相同就彈棧,否則進棧,最後棧中會剩下 \(k\) 個點,它們的顏色黑白交錯排列,不難得出最小相交次數即為 \(\frac k4\)

結論 \(2\):設 \(even_b\)\(even_w\)\(odd_b\)\(odd_w\) 分別表示偶位和奇位上黑色和白色點的個數,則最終棧中剩下的點數為 \(|even_b+odd_w-even_w-odd_b|\)
證明:考慮強行欽定每種顏色是進棧還是出棧,注意到如果 \(even_b+odd_w\ge even_w+odd_b\),我們可以把偶位上的黑點和奇位上的白點視為進棧(\(1\)),其他視為退棧(\(-1\)),這時候只要滿足任何時候棧容量(字首和)不為負數即可。而一個 \(1\) 不比 \(-1\) 少的數列一定存在一個迴圈位移滿足任意字首和不為負,所以這時候一定能找到一個合適的位置斷環,然後依次對棧進行操作,不難發現棧底的顏色是不變的,並且棧大小一定與當前已操作個數擁有相同的奇偶性,就證明了這麼欽定進出棧的正確性,\(even_b+odd_w<even_w+odd_b\) 也一樣。

進一步地,\(|even_b+odd_w-even_w-odd_b|\) 可以寫成 \(|2odd_w+2even_b-n|\),列舉 \(i=odd_w+even_b\) 滿足 \(2odd_w+2even_b-n\equiv0(\bmod 4)\),顯然滿足 \(odd_w+even_b=i\) 的方案數是一個組合數,可以直接計算,這樣可過簡單版。

對於複雜版,考慮這個組合數和式的通式:

\[\sum_{i=0}^z\binom zi|i-x|[i\bmod 2=y] \]

考慮大力分 \(i\le x\)\(i>x\)\(\binom zi\)\(i\binom zi\) 的和分開維護,\([i\bmod 2=y]\) 可用單位根反演等技巧搞掉。

注意到一次修改只會讓 \(z\)\(x\)\(1\) 或減 \(1\),對於 \(z\) 加一之後組合數字首和的變化,我們根據組合數遞推公式有:

\[\sum_{i=0}^x\binom {z+1}i=2\sum_{i=0}^x\binom zi-\binom zx \]

這樣就實現了組合數字首和在 \(z\) 加一的過程中的變化,要維護的其他值推導方式類似。

\(O(n+m)\)

Code

#include <bits/stdc++.h>

template <class T>
inline void read(T &res)
{
	res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	if (bo) res = ~res + 1;
}

const int N = 2e5 + 5, EI = 998244353, I2 = 499122177;

int n, m, fac[N], inv[N], ans, cnt, cnt0, cnt1, cnt_0, cnt_1, p2[N], i2[N],
posC, posCi, negC, negCi;
char s[N];

int C(int n, int m)
{
	if (m < 0 || m > n) return 0;
	return 1ll * fac[n] * inv[m] % EI * inv[n - m] % EI;
}

int main()
{
	int pos; char c; read(n); read(m);
	fac[0] = inv[0] = inv[1] = p2[0] = i2[0] = 1;
	for (int i = 1; i <= n; i++) fac[i] = 1ll * fac[i - 1] * i % EI,
		p2[i] = 2ll * p2[i - 1] % EI, i2[i] = 1ll * I2 * i2[i - 1] % EI;
	for (int i = 2; i <= n; i++) inv[i] = 1ll * (EI - EI / i) * inv[EI % i] % EI;
	for (int i = 2; i <= n; i++) inv[i] = 1ll * inv[i] * inv[i - 1] % EI;
	scanf("%s", s + 1);
	for (int i = 1; i <= n; i++)
	{
		if (s[i] == '?') cnt++;
		if ((i & 1) && s[i] == 'w') cnt0++;
		if ((i & 1) && s[i] != 'b') cnt_0++;
		if (!(i & 1) && s[i] == 'b') cnt1++;
		if (!(i & 1) && s[i] != 'w') cnt_1++;
	}
	for (int i = 0; i <= n; i++) if ((2 * i - n) % 4 == 0)
	{
		if (cnt0 + cnt1 > i || i > cnt_0 + cnt_1) continue;
		ans = (1ll * abs(2 * i - n) / 4 * C(cnt_0 + cnt_1
			- cnt0 - cnt1, i - cnt0 - cnt1) + ans) % EI;
	}
	std::cout << 1ll * ans * i2[cnt - 1] % EI << std::endl;
	for (int i = 0; i <= cnt_0 + cnt_1 - cnt0 - cnt1 &&
		i <= n / 2 - cnt0 - cnt1; i++)
		{
			int d = C(cnt_0 + cnt_1 - cnt0 - cnt1, i),
				di = 1ll * d * i % EI;
			posC = (posC + d) % EI; posCi = (posCi + di) % EI;
			if (i & 1) d = EI - d, di = EI - di;
			negC = (negC + d) % EI; negCi = (negCi + di) % EI;
		}
	while (m--)
	{
		read(pos);
		while ((c = getchar()) != 'w' && c != 'b' && c != '?');
		int mp = cnt_0 + cnt_1 - cnt0 - cnt1, mq = n / 2 - cnt0 - cnt1;
		if (pos & 1) cnt0 -= s[pos] == 'w', cnt0 += c == 'w',
			cnt_0 -= s[pos] != 'b', cnt_0 += c != 'b';
		else cnt1 -= s[pos] == 'b', cnt1 += c == 'b',
			cnt_1 -= s[pos] != 'w', cnt_1 += c != 'w';
		cnt -= s[pos] == '?'; cnt += c == '?';
		s[pos] = c;
		int np = cnt_0 + cnt_1 - cnt0 - cnt1, nq = n / 2 - cnt0 - cnt1;
		if (mp < np)
		{
			int t = C(mp, mq), u = mq & 1 ? EI - t : t;
			posCi = (2ll * posCi - 1ll * (mq + 1) * t % EI
				+ EI + posC) % EI;
			posC = (2ll * posC - t + EI) % EI;
			negCi = (1ll * (mq + 1) * u - negC + EI) % EI;
			negC = u;
		}
		else if (mp > np)
		{
			posC = 1ll * I2 * (C(np, mq) + posC) % EI;
			posCi = (1ll * (mq + 1) * C(np, mq) + posCi - posC + EI)
				% EI * I2 % EI;
			if (np)
			{
				int u = mq & 1 ? EI - C(np - 1, mq) : C(np - 1, mq);
				negC = u;
				negCi = (1ll * (mq + 1) * u - (np > 1 ? (mq & 1 ?
					EI - C(np - 2, mq) : C(np - 2, mq))
						: (mq >= 0)) + EI) % EI;
			}
			else negC = mq >= 0, negCi = 0;
		}
		if (mq < nq)
		{
			int d = C(np, nq), di = 1ll * d * nq % EI;
			posC = (posC + d) % EI; posCi = (posCi + di) % EI;
			if (nq & 1) d = EI - d, di = EI - di;
			negC = (negC + d) % EI; negCi = (negCi + di) % EI;
		}
		else if (mq > nq)
		{
			int d = C(np, mq), di = 1ll * d * mq % EI;
			posC = (posC - d + EI) % EI; posCi = (posCi - di + EI) % EI;
			if (mq & 1) d = EI - d, di = EI - di;
			negC = (negC - d + EI) % EI; negCi = (negCi - di + EI) % EI;
		}
		int resP = (1ll * nq * posC + (np ? 1ll * np * p2[np - 1] : 0)
			- posCi + EI) % EI,
			resN = (1ll * nq * negC + EI - (np == 1) - negCi + EI) % EI;
		resP = (1ll * resP - 1ll * nq * (p2[np] - posC + EI)
			% EI - posCi + EI + EI) % EI;
		resN = (1ll * resN - 1ll * nq * ((!np) - negC + EI)
			% EI - negCi + EI + EI) % EI;
		int res = nq & 1 ? 1ll * (resP - resN + EI) * I2 % EI
			: 1ll * (resP + resN) * I2 % EI;
		printf("%d\n", 1ll * i2[cnt] * res % EI);
	}
	return 0;
}

1458D

題意

給定一個 \(01\)\(s\),每次可以選一個 \(0\) \(1\) 個數相等的子串,將這個子串 \(01\) 取反之後再翻轉。

求使用這種操作能得到的最小字典序串。

多測,資料組數和每組資料的串長之和都不超過 \(5\times10^5\)

Solution

下面 \(n\)\(|s|\)

\(0\) 視為 \(-1\),字首和為 \(sum_i\)(下標範圍 \(0\sim n\)),可以發現這個操作相當於選一個 \(i<j\) 使得 \(sum_i=sum_j\),對 \(sum\) 陣列翻轉區間 \([i,j]\)

考慮一個 \(ans\) 數組合法的條件:

結論:為一個數組 \(a\) 整出一個無序二元組集合 \(\{(a_i,a_{i+1}),0\le i<n\}\),則 \(ans\) 合法當且僅當 \(ans_0=sum_0\)\(ans_n=sum_n\),且 \(ans\) 的二元組集合和 \(sum\) 的二元組集合相等。

證明:

必要性:由於翻轉區間的兩端點 \(sum\) 相等,所以翻轉操作不會改變相鄰數對集合。
充分性:假如當前串的前 \(i\) 位已經等於 \(ans\),要整第 \(i+1\) 位,可以發現只要當前串第 \(i\) 位之後有一個和 \((ans_i,ans_{i+1})\) 相等的相鄰數對,我們一定可以把 \(ans_{i+1}\) 搬過來,這樣第 \(i+1\) 位就符合 \(ans_{i+1}\) 了。

考慮將 \(sum_i\)\(sum_{i+1}\) 連無向邊,要求的就是從 \(0\)\(sum_n\) 的一條字典序最小的尤拉路徑。

按字典序從左往右貪心,任何時候保證沒走過的邊構成的圖連通即可。

由於一條邊連線的兩個點編號差為 \(1\),可以方便地維護當前圖的連通性,總複雜度 \(O(\sum n)\)

Code

#include <bits/stdc++.h>

template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}

template <class T>
inline T Max(const T &a, const T &b) {return a > b ? a : b;}

const int N = 5e5 + 5, E = 5e5, M = N << 1;

int n, sum[N], cnt[M];
char s[N];

void work()
{
	scanf("%s", s + 1); n = strlen(s + 1);
	for (int i = 1; i <= n; i++) sum[i] = sum[i - 1]
		+ (s[i] == '1' ? 1 : -1);
	for (int i = -n; i <= n; i++) cnt[i + E] = 0;
	for (int i = 1; i <= n; i++)
		cnt[(s[i] == '1' ? sum[i - 1] : sum[i]) + E]++;
	int L = 0, R = 0, cur = E;
	for (int i = 1; i <= n; i++) L = Min(L, sum[i]),
		R = Max(R, sum[i]);
	L += E; R += E;
	for (int i = 1; i <= n; i++)
		if (cur == L) putchar('1'),
			cnt[L]--, L += !cnt[L], cur++;
		else if (cur == R) putchar('0'),
			cnt[R - 1]--, R -= !cnt[R - 1], cur--;
		else if (cnt[cur - 1] > 1) cur--, cnt[cur]--, putchar('0');
		else cnt[cur]--, cur++, putchar('1');
	puts("");
}

int main()
{
	int T; std::cin >> T;
	while (T--) work();
	return 0;
}