ad-hoc 学习笔记

构造

P9635 「yyOI R1」youyou 的异或

题目链接

首先观察到 \(\forall k \in \mathbb Z,2k \oplus 2k+1=1\)。于是推出 \(\forall k \in \mathbb,4k \oplus 4k+1 \oplus 4k+2 \oplus 4k+3=0\),有个特殊情况 \(1 \oplus 2 \oplus 3=0\)(此时 \(k=0\),第一项为 \(0\) 舍去)。那么考虑对于 \(n \bmod 4\) 分类讨论:

  • \(n \equiv 0 \pmod 4\):此时 \(n=4k\),于是有 \(\oplus_{i=1}^{n-1} i=0\),所以直接就是 \(1,2,...,n\)

  • 其余三种情况稍微复杂一些,但同理可推。

代码
#include <bits/stdc++.h>
using namespace std;

int T, n;

int main() {
	scanf("%d", &T);
	while(T--) {
		scanf("%d", &n);
		if(n % 4 == 0) {
			for(int i = 1; i <= n; i++) cout << i << " ";
			cout << "\n";
		}
		else if(n % 4 == 1) {
			if(n == 1) cout << "1\n";
			else {
				for(int i = 2; i < n - 1; i++) cout << i << " ";
				cout << n << " " << n + 1 << " " << n + 2 << "\n";
			}
		}
		else if(n % 4 == 2) {
			if(n == 2) cout << "1 3\n";
			else {
				for(int i = 1; i < n; i++) cout << i << " ";
				cout << n + 1 << "\n";
			}
		}
		else {
			if(n == 3) cout << "-1\n";
			else {
				for(int i = 2; i < n; i++) cout << i << " ";
				cout << n + 1 << " " << n + 2 << "\n";
			}
		}
	}
	return 0;
}

总结

其实这题最重要的一步就是发现第一步的性质,后面的推到都顺理成章,只是复杂一些。

对于这题的破题口:看到让 \(n\) 个不同的数的异或和为 \(n\),不难想到在前面构造一堆数使得它们的异或和为 \(0\),于是可以关联到第一步的性质。


ARC209B Minimize Even Palindrome

题目链接

首先发现偶回文串中必定有两个相邻且相同的字符,所以有个想法就是让相邻字符尽可能不同。用 \(buc\) 存储每个字符出现了多少次,假设出现字数是 \(k\)。然后分类讨论:

  • \(buc_k>len-buc_k\):我们无法构造出一组没有偶回文串的字符串,所以考虑将 \(k\) 平均分配,在中间插入其他字符分隔。但是分配长度时偶数的时候,如果两边分隔字母相等就会生成更多的偶数回文串,所以我们可以将两个长度为 \(2 \times l\) 的偶回文串改为两个长度分别为 \(2 \times l−1\)\(2 \times l+1\) 的奇回文串,然后发现这样修改偶回文串数量不会增加变。最后可能剩下一个偶回文串,放在字符串的一边即可。

  • \(buc_k \le len-buc_k\):我们可以构造出一个相邻字符都不相同的字符串。

最后我觉得这道题还有个难点是代码不好写,如果实现不够巧妙可能会写很长。

代码
#include <bits/stdc++.h>
using namespace std;

const int maxn = 2e5 + 5;

int T, buc[30], cnt[maxn];
char s[maxn];
vector <char> mid[maxn];

int main() {
	scanf("%d", &T);
	while(T--) {
		scanf("%s", s + 1);
		int n = strlen(s + 1);
		for(int i = 0; i < 26; i++) buc[i] = 0;
		for(int i = 1; i <= n; i++) buc[s[i] - 'a']++;
		int k = 0;
		for(int i = 0; i < 26; i++) if(buc[k] < buc[i]) k = i;
		if(buc[k] > n - buc[k]) {
			int d = n - buc[k] + 1;
			for(int i = 1; i <= d; i++) cnt[i] = buc[k] / d;
			int rest = buc[k] % d;
			for(int i = 1; i <= rest; i++) cnt[i]++;
			for(int i = 2; i <= d; i++) if(!(cnt[i - 1] & 1) && !(cnt[i] & 1)) cnt[i - 1]--, cnt[i]++;
			for(int i = 2; i < d; i++) if(!(cnt[i] & 1)) swap(cnt[i], cnt[1]);
			int pos = 0;
			for(int i = 1; i <= d; i++) {
				while(cnt[i]--) printf("%c", k + 'a');
				if(i < d) {
					while(!buc[pos] || pos == k) pos++;
					printf("%c", pos + 'a');
					buc[pos]--;
				}
			}
			puts("");
		}
		else {
			int d = buc[k] + 1;
			for(int i = 1; i <= d; i++) mid[i].clear();
			int pos = 0;
			for(int i = 0; i < 26; i++) {
				if(i == k) continue;
				while(buc[i]--) {
					mid[pos + 1].emplace_back(i + 'a');
					pos = (pos + 1) % d;
				}
			}
			for(int i = 1; i <= d; i++) {
				for(char j : mid[i]) printf("%c", j);
				if(i < d) printf("%c", k + 'a');
			}
			puts("");
		}
	}
	return 0;
}

P8866 [NOIP2022] 喵了个喵

题目链接

tag:构造、分类讨论、模拟。

\(k=2n-2\)

不难想到每两种卡牌用一个栈,则最后会空出一个栈。如果排队出现 \(\text{abab\ldots}\) 最后就无法消除了,所以我们应该使用第二种操作和空栈避免这种情况。

\(n=2\)

分析一下只有两个栈分别为 \(\text{ab}\)(右边是栈顶)和 \(\text{c}\) 这一种情况是不一样的。分类讨论一下下一张牌是什么即可。

正解

其实就是以上两种情况合起来。有了上面的想法,不难发现每个栈内最多右两张牌。加入新牌时,如果这种牌在牌堆中出现过,那么我们一定可以将其消除。然后考虑如何加牌。加牌的唯一一种特殊情况是有 \(n-1\) 个牌堆放了两张牌,剩余一个空牌堆。这时我们应构造一种方法使得加入这张牌后仍有一个空牌堆。我们先暂时将 \(a_i\) 放到空牌堆,记 \(a_j\)\(i\) 后最先出现的在 \(n\) 个牌堆底出现的卡牌,那么此时我们一定可以操作出一个空牌堆。具体地,需分类讨论:

  • \(\exists l \in (i,j)\),使得 \(a_l\)\(a_j\) 上面的牌,按正常的操作方法操作即可。

  • 否则,记 \(pos_{t}\)\(t\) 所在的牌堆,则我们只需将 \(a_i\) 从空牌堆里拿出来(撤销),放到 \(pos_{a_l}\),然后将 \(a_j\) 放在那个空牌堆,然后对 \(pos_t\) 和空牌堆进行操作二即可。中间可能有其他操作,先记录一下即可。

代码特别难写。

代码
#include <bits/stdc++.h>

#define pii pair <int, int>
#define mp make_pair
#define eb emplace_back

using namespace std;

char *p1, *p2, buf[1 << 20];
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin), p1 == p2) ? EOF : *p1++)

template <typename T> void read(T &x) {
	x = 0; T f = 1; char c = gc();
	while(c < '0' || '9' < c) f = (c == '-') ? -1 : 1, c = gc();
	while('0' <= c && c <= '9') x = x * 10 + (c & 15), c = gc();
	x *= f;
}

template <typename T> void write(T x) {
	if(x < 0) putchar('-'), x = -x;
	static short st[40], top(0);
	do st[++top] = x % 10; while(x /= 10);
	while(top) putchar(st[top--] | 48);
}

const int maxn = 305, maxm = 2e6 + 5, maxk = 605;

int T, n, m, k, a[maxm];
int pos[maxk], st[maxn][4], t[maxn];
bool flag[maxk];
set <int> s[3];

void add(int x, int v) {
	pos[v] = x;
	s[t[x]].erase(x);
	st[x][++t[x]] = v;
	s[t[x]].insert(x);
	if(t[x] == 1) flag[v] = 1;
}

void del_front(int x) {
	pos[st[x][t[x]]] = 0;
	if(t[x] == 1) flag[st[x][t[x]]] = 0;
	s[t[x]].erase(x);
	t[x]--;
	s[t[x]].insert(x);
}

void del_back(int x) {
	pos[st[x][1]] = 0, flag[st[x][1]] = 0;
	s[t[x]].erase(x);
	if(t[x] == 2) st[x][1] = st[x][2];
	t[x]--;
	s[t[x]].insert(x);
	if(t[x]) flag[st[x][1]] = 1;
}

int main() {
	read(T);
	while(T--) {
		read(n), read(m), read(k);
		for(int i = 1; i <= m; i++) read(a[i]);
		s[0].clear();
		for(int i = 1; i <= n; i++) s[0].insert(i);
		vector <pii> ans;
		for(int i = 1; i <= m; i++) {
			if(pos[a[i]]) {
				int x = pos[a[i]];
				if(st[x][t[x]] == a[i]) ans.eb(mp(1, x)), del_front(x);
				else {
					int y = *s[0].begin();
					ans.eb(mp(1, y)), ans.eb(mp(2, x * n + y));
					del_back(x);
				}
			}
			else {
				if(!s[1].empty()) {
					int x = *s[1].begin();
					ans.eb(mp(1, x)), add(x, a[i]);
				}
				else if(s[0].size() > 1) {
					int x = *s[0].begin();
					ans.eb(mp(1, x)), add(x, a[i]);
				}
				else {
					int x = *s[0].begin();
					add(x, a[i]);
					int j = i + 1;
					while(!flag[a[j]]) j++;
					int o = pos[a[j]], opt = 2;
					for(int l = i + 1; l < j; l++) if(a[l] == st[o][2]) opt = 1;
					vector <pii> res;
					for(int l = i + 1; l < j; l++) {
						if(pos[a[l]]) {
							int y = pos[a[l]];
							res.eb(mp(1, y));
							del_front(y);
						}
						else {
							int y = *s[1].begin();
							if((opt == 1 && y == o) || (opt == 2 && y == x)) y = *s[1].rbegin();
							res.eb(mp(1, y));
							add(y, a[l]);
						}
					}
					if(t[o] == 1) {
						ans.eb(mp(1, x));
						for(pii l : res) ans.eb(l);
						ans.eb(mp(1, o));
						del_front(o);
					}
					else {
						del_back(x);
						ans.eb(mp(1, o));
						del_back(o);
						add(o, a[i]);
						for(pii l : res) ans.eb(l);
						ans.eb(mp(1, x)), ans.eb(mp(2, x * n + o));
					}
					i = j;
				}
			}
		}
		write(ans.size()), putchar('\n');
		for(pii i : ans) {
			int x = i.first, y = i.second;
			write(x), putchar(' ');
			if(x == 1) write(y);
			else {
				int u = y / n, v = y % n;
				if(!v) u--, v = n;
				write(u), putchar(' '), write(v);
			}
			putchar('\n');
		}
	}
	return 0;
}

AT_agc059_d [AGC059D] Distinct Elements on Subsegments

题目链接

定义:\(a_i\) 的前驱为 \(i\) 之前最后一个与 \(a_i\) 相等的数,后继为 \(i\) 之后第一个与 \(a_i\) 相等的数。

首先容易观察到 \(\forall i \in [1,n),|b_i-b_{i+1}|\le1\)。然后考虑如何将 \(b\) 数组的变化表示出来。记 \(l_i\) 表示对于 \(j \in [i-k+1,i)\),有没有 \(a_j=a_i\)(有记为 \(1\),否则为 \(0\)),\(r_i\) 表示对于 \(j \in (i,i+k-1]\),有没有 \(a_j=a_i\)。分类讨论:

  • \(b_i=b_{i+1}\)\(r_i=l_{i+k}\)

  • \(b_i=b_{i+1}-1\)\(r_i=0 \land l_{i+k}=1\)

  • \(b_i=b_{i+1}+1\)\(r_i=1 \land l_{i+k}=0\)

然后我们考虑 \(l,r\) 到底代表了什么。因为 \(l_i\) 记录了 \(i\) 之前的 \(k-1\) 个位置中是否有与 \(a_i\) 相等的数,不难想到 \(l_i\) 记录的实际上就是 \(i\) 的前驱与 \(i\) 的距离是否 \(<k\)。同理,\(r_i\) 记录的实际上就是 \(i\) 的后继与 \(i\) 的距离是否 \(<k\)。因为前驱和后继是一一对应的,所以 \(l\) 中的 \(0\)\(r\) 中的 \(0\) 实际上也是一一对应的!即如果 \(l_i\)\(r_j\) 是对应的,那么则有 \(l_i=r_j=0 \land i-j<k\),换句话说就是 \(a_j\)\(a_i\) 的前驱。这里可以直接用一个简单的贪心将 \(0\) 匹配:\(l\) 中的第 \(i\)\(0\)\(r\) 中的第 \(i\)\(0\) 匹配。

经过上面的分析我们知道了如果有了 \(l,r\) 可以还原出 \(a\),现在考率如何求 \(l,r\)。记 \(pl_i\)\(l\) 中第 \(i\)\(0\) 出现的位置,\(pr_i\)\(r\) 中第 \(i\)\(0\) 出现的位置,那么 \(\forall i \in [1,cnt_0],pl_i-pr_i \in [1,k)\)。我们应尽量满足这个条件。按照上文的方法,我们只有 \(b_i=b_{i+1}\) 需特殊处理,分类讨论:

  • \(b_i=b_{i+1}=k\)\(r_i=l{i+k}=1\)

  • \(b_i=b_{i+1}<k\)\(i \sim i+k\) 中至少有 \(2\) 个相同的数,即至少有一对 \(l,r\) 等于 \(0\)。假设 \(r_i,l_{i+k}=0\),若不考虑 \(r_i,l_{i+k}\) 时匹配,考率后一定依然匹配;若不匹配,则考虑后有可能匹配。所以我们应贪心地让 \(r_i=l_{i+k}=0\)

最后还剩下 \(l_{1,\ldots,k}\)\(r_{n,\ldots,n+k-1}\) 没有填,直接贪心地让前 \(b_1\)\(l\) 和后 \(b_n\)\(r\)\(1\) 即可。

代码
#include <bits/stdc++.h>

using namespace std;

const int maxn = 2e5 + 5, maxl = 4e5 + 5;

int T, n, k, a[maxl], b[maxn];
int l[maxl], r[maxl], pl[maxl], pr[maxl];
int pre[maxl];

int main() {
	scanf("%d", &T);
	while(T--) {
		scanf("%d%d", &n, &k);
		for(int i = 1; i <= n; i++) scanf("%d", &b[i]);
		bool flag = 0;
		for(int i = 1; i < n; i++) {
			if(abs(b[i] - b[i + 1]) > 1) {
				flag = 1;
				break;
			}
			if(b[i] < b[i + 1]) r[i] = 0, l[i + k] = 1;
			else if(b[i] > b[i + 1]) r[i] = 1, l[i + k] = 0;
			else if(b[i] == k) r[i] = 1, l[i + k] = 1;
			else r[i] = 0, l[i + k] = 0;
		}
		if(flag) {
			puts("No");
			continue;
		}
		for(int i = 1; i <= k; i++) {
			l[i] = (i <= b[1]);
			r[n + k - i] = (i <= b[n]);
		}
		int l0 = 0, r0 = 0;
		for(int i = 1; i < n + k; i++) {
			if(!l[i]) pl[++l0] = i;
			if(!r[i]) pr[++r0] = i;
		}
		if(l0 != r0) {
			puts("No");
			continue;
		}
		for(int i = 1; i < n + k; i++) pre[i] = 0;
		for(int i = 1; i <= l0; i++) {
			int t = pl[i] - pr[i];
			if(t < 1 || t >= k) {
				flag = 1;
				break;
			}
			pre[pl[i]] = pr[i];
		}
		if(flag) {
			puts("No");
			continue;
		}
		int num = 0;
		for(int i = 1; i < n + k; i++) {
			if(pre[i]) a[i] = a[pre[i]];
			else a[i] = ++num;
		}
		puts("Yes");
		for(int i = 1; i < n + k; i++) printf("%d ", a[i]);
		puts("");
	}
	return 0;
}

乱搞

CF364D Ghd

题目链接

tag:随机化。

这题第一眼看上去完全不可做,但是注意到 ghd 有整除至少一半数的性质,也就是说我们随机取一个数,有 \(\frac{1}{2}\) 概率是最终答案的倍数,所以考虑随机化。

每次随机化随机一个 \(x=a_i\),记录 \(x\) 的所有因数 \(s_j\)。如果 \(s_j\) 在超过一半的 \(a_i\) 中出现则可能是答案。但是直接判断每个因数在每个 \(a_i\) 中出现没有时间复杂度会炸,所以我们将 \(s\) 从小到大排序,每次只标记 \(\gcd(x,a_i)\),做完后再把每个 \(s\) 的答案下传到它的因子中。

代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;

const int maxn = 1e6 + 5;

int n, buc[maxn];
ll a[maxn], s[maxn];

mt19937 rng(time(0));

int main() {
	srand(time(0));
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) scanf("%lld", &a[i]);
	ll ans = 1;
	for(int t = 1; t <= 10; t++) {
		ll x = a[rng() % n + 1];
		int lim = sqrt(x), tot = 0;
		for(int i = 1; i <= lim; i++) {
			if(x % i) continue;
			s[++tot] = i;
			if(x / i != i) s[++tot] = x / i;
		}
		for(int i = 1; i <= tot; i++) buc[i] = 0;
		sort(s + 1, s + tot + 1);
		for(int i = 1; i <= n; i++)
			buc[lower_bound(s + 1, s + tot + 1, __gcd(x, a[i])) - s]++;
		for(int i = 1; i <= tot; i++)
			for(int j = 1; j < i; j++)
				if(s[i] % s[j] == 0) buc[j] += buc[i];
		for(int i = tot; i; i--) {
			if(buc[i] * 2 >= n) {
				ans = max(ans, s[i]);
				break;
			}
		}
	}
	printf("%lld\n", ans);
	return 0;
}

总结

  • 看到 大于一半的数 或对一道题完全没有思路时就应想想随机化的方法。

  • rand() 不够随机!建议使用 mt19937 rng(time(0)),但注意这个的范围是 \([0,2^{32}-1]\)int 存不下!

  • 在本题中 \(a_i \le 10^{12}\),记 \(m\)\(a_i\) 的因数个数,\(m\) 最大可达到 \(10^4\) 左右,每次 \(m^2\) 下传答案的理论最大时间复杂度其实是不对的,但出题人没卡这个,题解区好像也全是这样的。


P15385 回文回文回 IV / paliniv

题目链接

tag:分类讨论,组合数学。

首先直接按奇偶分类讨论,考虑一个序列 \(a\) 什么时候合法:

  • 奇数:\(\forall i \in [1,\lfloor \frac{n}{2} \rfloor],a_{i+1}+a_{n-i+1}=0\)

  • 偶数:经观察发现与奇数时相同。

如果你直接继续分类讨论做的话会发现 \(0\) 的情况很麻烦,于是考虑先不考虑 \(0\)(也可以从特殊性质得到这个想法)。记 \(cnt_{i}\) 表示 \(a\) 中的 \(x,-x\) 有多少对(\(x \in [1,n]\)),那么不考虑 \(0\) 的答案即为:

\[(\sum cnt_i)! \times \prod cnt_i! \times 2^{cnt_i} \]

然后考虑加上 \(0\)。假设有 \(c_0\)\(0\),那么首先这 \(c_0\)\(0\) 任意排列会使答案乘 \(c_0!\)。然后考虑如何将这 \(c_0\)无序\(0\) 插入序列。我们发现我们已经统计好的序列是这样的(只是例子,可以是其他数):

\[1,,-5,9,\ldots,-1,1,\ldots,-9,5,-1 \]

把这个序列分成两半,两半的数字的绝对值是对称的,插入 \(0\) 后也是这样

\[\binom{(\sum cnt_i)+\lfloor\frac{c_0}{2} \rfloor}{\lfloor\frac{c_0}{2} \rfloor} \]

最后我们还需要单独考虑一下 \(a_1\),我们将数字两两配对后会多出来一个放到 \(a_1\)(如果是 \(0\) 就不用管,因为已经算过了),这个 \(a_1\) 可以是任意一个与它数值相同的数字,所以答案在乘上 \(cnt_{a_1}\)

代码
#include <bits/stdc++.h>
using namespace std;

const int maxn = 1e5 + 5;
const int mod = 998244353;

int T, n, a, buc[maxn], now[maxn];
int fac[maxn], inv[maxn], p2[maxn];

int quick_pow(int a, int b) {
	int res = 1;
	while(b) {
		if(b & 1) res = 1ll * res * a % mod;
		a = 1ll * a * a % mod;
		b >>= 1;
	}
	return res;
}

void init() {
	fac[0] = inv[0] = p2[0] = 1;
	for(int i = 1; i < maxn; i++) {
		fac[i] = 1ll * fac[i - 1] * i % mod;
		p2[i] = 2ll * p2[i - 1] % mod;
	}
	inv[maxn - 1] = quick_pow(fac[maxn - 1], mod - 2);
	for(int i = maxn - 2; i; i--) inv[i] = 1ll * inv[i + 1] * (i + 1) % mod;
}

int binom(int a, int b) {
	return 1ll * fac[a] * inv[b] % mod * inv[a - b] % mod;
}

int main() {
	init();
	scanf("%d", &T);
	while(T--) {
		scanf("%d", &n);
		for(int i = 1; i <= n; i++) buc[i] = now[i] = 0;
		int c0 = 0;
		for(int i = 1; i <= n; i++) {
			scanf("%d", &a);
			if(!a) c0++;
			else if(a > 0) buc[a]++, now[a]++;
			else now[-a]--;
		}
		int sum = 0;
		for(int i = 1; i <= n; i++) sum += abs(now[i]);
		if(sum > 1) puts("0");
		else if(!(n & 1) && !c0) puts("0");
		else {
			int res = fac[c0], tot = 0;
			for(int i = 1; i <= n; i++) {
				int t = buc[i];
				if(now[i]) {
					if(now[i] > 0) t--, res = 1ll * res * buc[i] % mod;
					else res = 1ll * res * (buc[i] + 1) % mod;
				}
				tot += t;
				res = 1ll * res * fac[t] % mod * p2[t] % mod;
			}
			res = 1ll * res * binom(n / 2 - (!(n & 1)), tot) % mod * fac[tot] % mod;
			printf("%d\n", res);
		}
	}
	return 0;
}

P15776 [JAG 2025 Summer Camp #2] All Copy Paste

题目链接

首先我们可以通过 \(\log m\) 次操作把序列长度变为 \(m\)。直接暴力模拟。记 \(a\) 为这个长度为 \(m\) 的序列。时间复杂度 \(O(n \log m)\)

然后考虑对于一个长度为 \(m\) 的做一次操作会怎样。若 \(x_q \ge m\) 则无影响,否则相当于在序列第 \(x_q\) 位后插入一个原序列长度为 \(m-x_q\) 的前缀。

直接按操作顺序做看着没前途,所以考虑倒着做, 求出序列最终的每一位是 \(a\) 中的哪一位。对于一个位置 \(i\),若 \(i<x_q\),则这次操作前这个位置是 \(i-x_q\),否则这个位置在操作前后不变。所以我们可以把序列分为若干段,每一段内位置是连续的,所以可以用优先队列维护每一段的起始位置和长度。具体地,若当前长度 \(len>x_q\),则把这一段分为长度为 \(x_q\)\(m-x_q\) 的两段,起始位置也相应改变即可。因为最多分 \(m\) 次,所以这部分时间复杂度为 \(O(m \log n)\)

代码
#include <bits/stdc++.h>

#define ll long long
#define eb emplace_back
#define pii pair <int, int>
#define mp make_pair

using namespace std;

char *p1, *p2, buf[1 << 20];
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin), p1 == p2) ? EOF : *p1++)

template <typename T> void read(T &x) {
	x = 0; T f = 1; char c = gc();
	while(c < '0' || '9' < c) f = (c == '-') ? -1 : 1, c = gc();
	while('0' <= c && c <= '9') x = x * 10 + (c & 15), c = gc();
	x *= f;
}

template <typename T> void write(T x) {
	if(x < 0) putchar('-'), x = -x;
	static short st[40], top(0);
	do st[++top] = x % 10; while(x /= 10);
	while(top) putchar(st[top--] | 48);
}

const int maxn = 1e6 + 5;

int n, m, q, a[maxn << 1], t[maxn], op[maxn];

int main() {
	read(n), read(m), read(q);
	int len = min(n, m);
	for(int i = 1; i <= len; i++) a[i] = i;
	ll k; int tot = 0;
	while(q--) {
		read(k);
		if(k >= m) continue;
		if(len < m) {
			for(int i = 1; i <= len; i++) t[i] = a[i];
			for(int i = len; i > k; i--) a[i + len] = a[i];
			for(int i = 1; i <= len; i++) a[k + i] = t[i];
			len = min(len * 2, m);
		}
		else op[++tot] = k;
	}
	if(tot) {
		priority_queue <pii> q;
		q.push(mp(m, 1));
		for(int i = tot; i; i--) {
			vector <pii> rec;
			while(!q.empty() && q.top().first > op[i]) {
				pii x = q.top(); q.pop();
				int s = x.second, l = x.first;
				rec.eb(mp(op[i], s));
				rec.eb(mp(l - op[i], s + op[i]));
			}
			for(pii j : rec) q.push(j);
		}
		while(!q.empty()) {
			pii x = q.top(); q.pop();
			int s = x.second, l = x.first;
			for(int i = 1; i <= l; i++) t[s + i - 1] = a[i];
		}
		for(int i = 1; i <= m; i++) a[i] = t[i];
	}
	for(int i = 1; i <= len; i++) write(a[i]), putchar(' ');
	return 0;
}

总结

这道题主要是想到倒着维护每一答案序列每一位对应原序列的哪个位置。


交互题

CF1854D Michael and Hotel

题目链接

首先 \(n\) 个房间形成了一个内向基环树森林。不难发现如果两人在同一棵基环树上即可相遇。于是题目即求包含 \(1\) 的基环树里有哪些点。

既然是基环树,那么先考虑找环。我们发现,从 \(1\) 出发,基环树的环上相邻两个点距离之差一定等于 \(1\),而从 \(1\) 出发走 \(n+1\) 步一定会到环上。于是我们先从大到小枚举步数 \(i\),用类似分治的方法找从 \(1\) 出发走 \(i\) 步会到哪个点,如果当前点是第二次被找到了,那么环就找完了。

假设环长为 \(B\),那么我们的询问大约次数为 \(B \log n\)\(B\) 可与 \(n\) 同阶,所以过不了。但我们发现如果我们知道了环上连续的 \(k\) 个点,那么环上一定存在另外 \(k\) 个连续的点到这 \(k\) 个点(并且是与已知的 \(k\) 个点相邻的)的距离为 \(k\)。(实际找的时候可能会找到树上的点,直接加入答案集合,不用特殊处理)。这意味着我们可以用 \(O(n)\) 次询问将 \(k\) 翻倍。

我们知道 \(\log x\) 函数的图像是单增的,且增长速度越来越慢,即在 \(x\) 很小的的地方增长较快,所以考虑用类似根号分治的思想结合上面两个想法。假设我们暴力找了 \(d\) 个环上连续的点,那么我们的总询问次数大约是 \((\log n - \log d) \cdot n+d \log n\)。打表发现 \(d\)\(80\) 的时候答案最小,随便优化一下就可以过了。

代码
#include <bits/stdc++.h>
#define eb emplace_back
using namespace std;

const int maxn = 505;

int n; 
bool vis[maxn];

int query(int u, int k, vector <int> v) {
	cout << "? " << u << " " << k << " " << v.size() << " ";
	for(int i : v) cout << i << " ";
	cout << "\n"; fflush(stdout);
	int res; cin >> res;
	return res; 
}

int dfs(int l, int r, int k) {
	if(l == r) return l;
	int mid = (l + r) >> 1;
	vector <int> t;
	for(int i = l; i <= mid; i++) t.eb(i);
	if(query(1, k, t)) return dfs(l, mid, k);
	else return dfs(mid + 1, r, k);
}

int main() {
	cin >> n;
	vector <int> ans;
	for(int i = 1; i <= n; i++) vis[i] = 0;
	int tot = 80;
	for(int i = n + 1; ; i--) {
		int x = dfs(1, n, i);
		if(vis[x]) break;
		vis[x] = 1;
		ans.eb(x);
		tot--;
		if(!tot) break;
	}
	if(!tot) for(int i = 80; i <= n; i <<= 1) {
		int cnt = 0;
		for(int j = 1; j <= n; j++) {
			if(vis[j]) continue;
			if(query(j, i, ans)) {
				vis[j] = 1;
				ans.eb(j);
				cnt++;
			}
		}
		if(cnt < i) break;
	}
	for(int i = 1; i <= n; i++) {
		if(vis[i]) continue;
		if(query(i, n, ans)) ans.eb(i);
	}
	cout << "! " << ans.size() << " ";
	for(int i : ans) cout << i << " ";
	return 0;
}

贪心


P8957 「CGOI-3」巫泡弹弹乐

题目链接

做法一

看到数据范围没有什么想法,于是思考是否有贪心性质。不难发现若对于 \(i,j\)\(a_i \le a_j \land b_i \le b_j\),那么则可以将 \(i,j\) 连边并删除 \(j\),边权为 \(a_j+b_j\)

然后考虑将剩下的点按 \(a\) 从小到大排序,那么就会发现 \(b\) 是从大到小的!然后考虑按照 kruskal 的想法,就会发现现在最短的边一定是相邻的两点的连边。连边后可将这两个点看成一个点,然后归纳法可发现相邻两点两两连边是最优的。

代码
#include <bits/stdc++.h>

#define ll long long
#define eb emplace_back
#define pii pair <int, int>
#define mp make_pair

using namespace std;

const int maxn = 1e6 + 5;

int n;

struct node {
	int a, b, id;
	
	bool operator< (const node &t) const {
		if(a != t.a) return a < t.a;
		return b < t.b;
	}
} s[maxn];

int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) scanf("%d", &s[i].a);
	for(int i = 1; i <= n; i++) scanf("%d", &s[i].b), s[i].id = i;
	sort(s + 1, s + n + 1);
	ll ans = 0;
	vector <pii> e;
	int last = 1;
	for(int i = 2; i <= n; i++) {
		e.eb(mp(s[last].id, s[i].id));
		if(s[last].b <= s[i].b) ans += s[i].a + s[i].b;
		else ans += s[last].b + s[i].a, last = i;
	}
	printf("%lld\n", ans);
	for(pii i : e) printf("%d %d\n", i.first, i.second);
	return 0;
}

做法二

考虑将点按 \(a\) 从小到大排序,然后依次加点。那么加点 \(i\) 时增加的边权的 \(a\) 一定来 \(a_i\),然后可贪心地取前 \(i-1\) 个点里 \(b\) 最小的点。

总结

看到这种的数据范围肯定要么是随机化要么是贪心,而这题显然不可以随机化,所以应思考有没有贪心性质。

对于做法一,主要是发现可将有偏序关系的点消除,然后后面的结论及证明就显然了。

对于做法二,主要就是想到依次加点的做法,正确性显然。

posted @ 2026-09-03 12:45  PrinceEvan  阅读(5)  评论(0)    收藏  举报