构造杂题选做
构造杂题选做
加强限制
CF618F Double Knapsack
给你两个大小为 \(n\) 的可重集 \(A, B\) ,其中每个元素的大小 \(x\in [1,n]\)。请分别找出一个 \(A, B\) 的子集,使得元素和相等。
\(n \leq 10^6\)
考虑将子集加强为区间。
令 \(sa, sb\) 为前缀和,则限制条件为 \(sa_{r_a} - sa_{l_a - 1} = sb_{r_b} - sb_{l_b - 1}\) ,即 \(sa_{r_a} - sb_{r_b} = sa_{l_a - 1} - sb_{l_b - 1}\) 。
钦定 \(sa_n \geq sb_n\) ,则对于每个 \(sb_i\) ,找到第一个不小于它的 \(sa_j\) ,则 \(sa_j - sb_i \in [0, n)\) ,而 \(i \in [0, n]\) ,由抽屉原理必有相同元素,取出来作为一组解即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7;
ll sa[N], sb[N];
int n;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%lld", sa + i), sa[i] += sa[i - 1];
for (int i = 1; i <= n; ++i)
scanf("%lld", sb + i), sb[i] += sb[i - 1];
map<ll, pair<int, int> > mp;
vector<int> ansa, ansb;
mp[0] = make_pair(0, 0);
bool flag = (sa[n] < sb[n]);
if (flag)
swap(sa, sb);
for (int i = 1, j = 0; i <= n; ++i) {
while (j <= n && sa[j] < sb[i])
++j;
if (mp.find(sa[j] - sb[i]) == mp.end())
mp[sa[j] - sb[i]] = make_pair(j, i);
else {
int la = mp[sa[j] - sb[i]].first, lb = mp[sa[j] - sb[i]].second;
ansa.resize(j - la), iota(ansa.begin(), ansa.end(), la + 1);
ansb.resize(i - lb), iota(ansb.begin(), ansb.end(), lb + 1);
break;
}
}
if (flag)
swap(ansa, ansb);
printf("%d\n", (int)ansa.size());
for (int it : ansa)
printf("%d ", it);
printf("\n%d\n", (int)ansb.size());
for (int it : ansb)
printf("%d ", it);
return 0;
}
CF741C Arpa’s overnight party and Mehrdad’s silent entering
有 \(n\) 对情侣围成一圈,将每个人赋上 \(1\) 或 \(2\) 的权值满足:
- 任意一对情侣权值不同。
- 任意连续三个人权值不能全相同。
\(n \leq 10^5\)
考虑将第二个限制加强为:对于 \((1, 2), (2, 3), \cdots, (n - 1, n)\) 这 \(n\) 对人权值均不相同。
这样所有限制都转化为二元限制,不难用二分图染色求解,接下来考虑正确性。
对于一个环,其边一定是相邻边和情侣边交替,故不存在奇环。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int a[N], b[N], col[N];
int n;
void dfs(int u) {
for (int v : G.e[u])
if (!col[v])
col[v] = (col[u] == 1 ? 2 : 1), dfs(v);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d%d", a + i, b + i);
G.insert(a[i], b[i]), G.insert(b[i], a[i]);
}
for (int i = 1; i <= n * 2; i += 2)
G.insert(i, i + 1), G.insert(i + 1, i);
for (int i = 1; i <= n * 2; ++i)
if (!col[i])
col[i] = 1, dfs(i);
for (int i = 1; i <= n; ++i)
printf("%d %d\n", col[a[i]], col[b[i]]);
return 0;
}
CF241D Numbers
给定 \(1 \sim n\) 的排列,从中选取一些数满足异或和为 \(0\) 且数位拼接起来的数能被质数 \(p\) 整除,构造一种方案或报告无解。
\(n, p \leq 5 \times 10^4\)
拼接起来的数能被 \(p\) 整除这个条件没有什么很好的性质,一个无敌的想法是考虑把它视为一个随机事件。
注意到 \(1 \sim 24\) 的子集异或和为 \(0\) 的方案数远大于模数的范围,只保留这些数暴力枚举子集即可做到一个较高的正确率。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 7, B = 24;
int a[N];
int n, p;
signed main() {
scanf("%d%d", &n, &p);
vector<int> vec;
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i);
if (a[i] <= B)
vec.emplace_back(i);
}
for (int i = 1; i < (1 << min(n, B)); ++i) {
int res1 = 0, res2 = 0;
for (int j = 0; j < vec.size(); ++j)
if (i >> j & 1)
res1 ^= a[vec[j]], res2 = (res2 * (a[vec[j]] >= 10 ? 100 : 10) + a[vec[j]]) % p;
if (!res1 && !res2) {
printf("Yes\n%d\n", __builtin_popcount(i));
for (int j = 0; j < vec.size(); ++j)
if (i >> j & 1)
printf("%d ", vec[j]);
puts("");
return 0;
}
}
puts("No");
return 0;
}
增量构造
N 宫格
定义一个 \(n \times n\) 的矩阵合法当且仅当每行、每列均为 \(1 \sim n\) 的排列。
给定递增序列 \(a_{1 \sim n}\) ,构造一个 \(a_n \times a_n\) 的矩阵满足每个左下角为 \((1, 1)\) 、右上角为 \((a_i, a_i)\) 的矩阵均合法,或告知无解。
\(a_n \leq 1000\)
首先不难发现若 \(a_{i + 1} < 2 a_i\) 则无解,接下来考虑从 \(x \times x\) 的矩阵推广到 \(y \times y\) 的矩阵,并且不能动 \(x \times x\) 的部分。
此时剩下的矩阵可以分为三个部分:两个 \(x \times (y - x)\) 的部分与一个 \((y - x) \times (y - x)\) 的部分。
先考虑两个 \(x \times (y - x)\) 的部分,考虑放入 \(x + 1 \sim y\) 的轮换。以 \(x = 3, y = 8\) 为例:
尝试在剩下的 \((y - x) \times (y - x)\) 的部分将 \(x + 1 \sim y\) 弄出一个比较对称的图案。
对于 \(2x + 1 \sim y\) ,考虑在左上角和右下角弄出若干条斜率为 \(1\) 的直线,其中一条直线的数字相同,左上角与两边的轮换对齐,右下角用于补全出现次数。
对于 \(x + 1 \sim 2x\) ,考虑构造若干条斜率为 \(-1\) 的直线,其中一条直线的数字升序。
具体的:
然后将剩下的 \((x, y)\) 按 \(x - y\) 分组后排序,依次填入 \(1 \sim x\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
int ans[N][N];
int a[N];
int n;
inline void solve(int x, int y) {
for (int i = 1; i <= x; ++i)
for (int j = x + 1; j <= y; ++j)
ans[i][j] = ans[j][i] = x + (i + j - x - 2) % (y - x) + 1;
if (y > x * 2) {
int cur = x * 2 + 1, bx = x + 1, by = x + 1;
do {
int i = bx, j = by;
for (int k = 1; k <= y - x * 2; ++k) {
ans[i++][j--] = cur;
if (j == x)
i = cur + 1, j = y;
}
if (cur <= x * 2 || cur == y)
++bx;
++by, cur = (cur == y ? x + 1 : cur + 1);
} while (cur != x * 2 + 1);
}
vector<pair<int, int> > vec;
for (int i = x + 1; i <= y; ++i)
for (int j = x + 1; j <= y; ++j)
if (!ans[i][j])
vec.emplace_back(i, j);
sort(vec.begin(), vec.end(), [](const pair<int, int> &a, const pair<int, int> &b) {
return a.first - a.second < b.first - b.second;
});
for (int i = 0, now = 1; i < vec.size(); ++i) {
if (i && vec[i].first - vec[i].second != vec[i - 1].first - vec[i - 1].second)
now = now % x + 1;
ans[vec[i].first][vec[i].second] = now;
}
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i)
if (a[i + 1] < a[i] * 2)
return puts("No"), 0;
for (int i = 1; i <= a[1]; ++i)
for (int j = 1; j <= a[1]; ++j)
ans[i][j] = (i + j - 2) % a[1] + 1;
for (int i = 1; i < n; ++i)
solve(a[i], a[i + 1]);
puts("Yes");
for (int i = 1; i <= a[n]; ++i) {
for (int j = 1; j <= a[n]; ++j)
printf("%d ", ans[i][j]);
puts("");
}
return 0;
}
P6644 [CCO 2020] Travelling Salesperson
给定一张 \(n\) 个点的完全图,每条边有 \(0\) 或 \(1\) 的权值。
对于每个点,构造一条从该点出发,经过每个点至少一次,且长度最短的路径,要求这条路径只能“切换”一次权值。
\(n \leq 2 \times 10^3\)
显然路径长度 \(\geq n - 1\) ,考虑构造长为 \(n - 1\) 的路径。
考虑增量法,假设对前 \(k\) 个点构造了 \(v_1 \to v_2 \to \cdots \to v_k\) 的路径,考虑加入第 \(k\) 个点。
- 若路径权值均等,则直接在首或尾插入即可。
- 否则考虑找到断点 \(d\) ,满足 \(v_1 \to \cdots \to v_d\) 边权相等,\(v_{d + 1} \to \cdots \to v_k\) 边权相等。若 \((v_d, k)\) 与 \((v_{d - 1}, v_d)\) 同色,则在 \(v_d\) 后面插入 \(k\) 即可,否则在 \(v_d\) 前面插入 \(k\) 即可。
用双向链表维护路径,动态维护端点,时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7;
int a[N][N];
char str[N];
int n;
signed main() {
scanf("%d", &n);
for (int i = 2; i <= n; ++i) {
scanf("%s", str + 1);
for (int j = 1; j <= i; ++j)
a[i][j] = a[j][i] = (str[j] == 'B');
}
for (int i = 1; i <= n; ++i) {
list<int> path = {i};
auto p = path.end();
for (int j = 1; j <= n; ++j) {
if (j == i)
continue;
if (p == path.end()) {
path.emplace_back(j), p = prev(prev(path.end()));
if (path.size() == 2 || a[*prev(p)][*p] == a[*p][*next(p)])
p = path.end();
} else {
if (a[*p][j] == a[*prev(p)][*p]) {
p = path.insert(next(p), j);
if (a[*prev(p)][*p] == a[*p][*next(p)]) {
++p;
if (p == prev(path.end()))
p = path.end();
}
} else {
p = path.insert(p, j);
if (a[*prev(p)][*p] == a[*p][*next(p)]) {
--p;
if (p == path.begin())
p = path.end();
}
}
}
}
printf("%d\n", n);
for (int it : path)
printf("%d ", it);
puts("");
}
return 0;
}
UOJ216. 【UNR #1】Jakarta Skyscrapers
给出一个大小为 \(2\) 的集合 \(S = \{ a, b \}\) ,每次可以从 \(S\) 中选两个不同的元素 \(x, y\) ,并将 \(|x - y|\) 添加到 \(S\) 中,试用 \(\leq 400\) 次操作使得 \(c \in S\) 成立,或判定无解。
\(a, b, c \leq 10^{18}\)
不难发现 \(S\) 中的数 \(k\) 一定满足 \(k = ax + by\) 且 \(k \leq \max(a, b)\) ,因此 \(\gcd(a, b) \nmid c\) 或 \(c > \max(a, b)\) 则无解。
先考虑已知 \(g = \gcd(a, b)\) 时如何构造出 \(c\) ,不妨钦定 \(a > b\) ,考虑一组操作 \((a, g), (a - g, g), (a, a - 2g)\) 即可得到 \(2g\) ,再用一组操作 \((a - 2g, 2g), (a, a - 4g)\) 即可得到 \(4g\) ,以此类推可以用 \(1 + 2 \log V\) 次操作得到 \(g\) 乘上 \(2\) 的次幂的各个值,然后通过二进制分解即可再用 \(3 \log V\) 次操作表示 \(c\) 。
接下来考虑用辗转相除法得到 \(g\) ,每次计算 \(a \bmod b = a - \lfloor \frac{a}{b} \rfloor b\) 时,采用类似的方法得到后者,然后递归处理。由于每次 \(\prod \lfloor \frac{a}{b} \rfloor \leq a\) ,因此该部分操作次数上界为 \(3 \log V\) 。
总操作次数 \(\leq 6 \log V\) ,由于常数问题实际会大一些。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
set<ll> st;
vector<pair<ll, ll> > ans;
ll a, b, c, g;
inline void update(ll x, ll y) {
if (x > y && st.find(x - y) == st.end())
ans.emplace_back(x, y), st.emplace(x - y);
}
inline void calc(ll a, ll b) {
update(a, b);
for (ll x = b; x * 2 < a; x <<= 1)
update(a - x, x), update(a, a - x * 2);
for (ll n = a / b, x = a; n; n ^= 1ll << __lg(n))
update(x, b << __lg(n)), x -= b << __lg(n);
}
signed main() {
scanf("%lld%lld%lld", &a, &b, &c);
if (a < b)
swap(a, b);
ll g = __gcd(a, b);
if (c > a || c % g)
return puts("-1"), 0;
else if (c == a)
return puts("0"), 0;
st = {a, b};
for (ll x = a, y = b; y; x %= y, swap(x, y))
calc(x, y);
calc(a, g);
for (ll n = (a - c) / g, x = a; n; n ^= 1ll << __lg(n))
update(x, g << __lg(n)), x -= g << __lg(n);
printf("%d\n", (int)ans.size());
for (auto it : ans)
printf("%lld %lld\n", it.first, it.second);
return 0;
}
化子问题
P10871 [COTS 2022] 皇后 Kraljice
有一个 \(n \times n\) 的棋盘,构造一组依次放置皇后的方案,满足每次放置时该格子被偶数个皇后攻击(忽略皇后之间的阻挡),最大化放置数量。
\(n \leq 2^{10}\)
先给结论:\(n \leq 2\) 时答案为 \(1\) ,否则 \(2 \mid n\) 时答案为 \(n^2 - 2\) ,否则答案为 \(n^2\) 。
先考虑偶数的构造。先考虑 \(n = 4\) 的情况,暴搜可以发现最优答案是 \(14\) ,可行的一组构造形如:
然后考虑增量构造,考虑一圈一圈向内构造,这样外圈不影响内圈,达到化子问题的目的。
先把上下两行构造出来,不难发现相邻一组每次可以如此构造:
并且这样不会影响后面的构造(只需考虑横竖攻击),但是构造到最后一组是对角线会产生影响,不难发现只要如此构造即可:
左右两侧是类似的,并且不用考虑对角线的影响。
接下来考虑奇数的构造,同样考虑一圈一圈向内构造。先把行构造出来,构造上下两个满的行是困难的,但是容易蛇形构造出这样的情况:
对于左右两列也是类似的: \((n - 1, n) \to (1, n - 1) \to (1, n - 2) \to (n - 2, n) \to \cdots\) 。
但是这样会出现除了左上角的三个角都放不了的情况,考虑在次外圈的一个角 \((2, n - 1)\) 处放一个,这样就可以按 \((1, n) \to (n, n) \to (n, 1)\) 的顺序放完最外圈了。
根据构造方式可以得知,次外圈放的这个角作为子问题的第一个角,递归到 \(n = 3\) 后特殊处理即可。
#include <bits/stdc++.h>
using namespace std;
int n;
void dfs1(int l, int r, int d) {
auto F = [&](int x, int y) {
if (!d)
printf("%d %d\n", x, y);
else if (d == 1)
printf("%d %d\n", n - y + 1, x);
else if (d == 2)
printf("%d %d\n", n - x + 1, n - y + 1);
else
printf("%d %d\n", y, n - x + 1);
};
if (r - l + 1 == 3) {
auto G = [&](int x, int y) {
F(l + x - 1, l + y - 1);
};
if (l == 1)
G(1, 1);
G(2, 3), G(2, 2), G(3, 1), G(1, 3), G(1, 2), G(3, 2), G(3, 3), G(2, 1);
return;
}
if (l == 1)
printf("%d %d\n", l, l);
for (int i = l + 1; i <= r - 1; ++i) {
if ((i - l) & 1)
F(r, i), F(l, i);
else
F(l, i), F(r, i);
}
for (int i = r - 1; i >= l + 1; --i) {
if ((r - i) & 1)
F(i, r), F(i, l);
else
F(i, l), F(i, r);
}
F(r - 1, l + 1), F(l, r), F(r, r), F(r, l);
dfs1(l + 1, r - 1, (d + 1) & 3);
}
void dfs2(int l, int r) {
auto F = [](int x, int y) {
printf("%d %d\n", x, y);
};
if (r - l + 1 == 4) {
auto G = [&](int x, int y) {
F(l + x - 1, l + y - 1);
};
G(1, 4), G(3, 3), G(3, 4), G(1, 1), G(4, 1), G(2, 3), G(1, 3),
G(2, 2), G(1, 2), G(4, 4), G(3, 1), G(4, 2), G(2, 1), G(3, 2);
return;
}
for (int i = l; i <= r - 3; i += 2)
F(l, i), F(r, i + 1), F(l, i + 1), F(r, i);
F(l, r - 1), F(l, r), F(r, r), F(r, r - 1);
for (int i = l + 1; i <= r - 2; i += 2)
F(i, l), F(i + 1, r), F(i + 1, l), F(i, r);
dfs2(l + 1, r - 1);
}
signed main() {
scanf("%d", &n);
if (n <= 2)
return puts("1\n1 1"), 0;
printf("%d\n", n & 1 ? n * n : n * n - 2);
if (n & 1)
dfs1(1, n, 0);
else
dfs2(1, n);
return 0;
}
P6892 [ICPC 2014 WF] Baggage
有一个无限长的数轴,其中 \(1 \sim 2n\) 有字母,奇数位置为
B
,偶数位置为A
。一次操作可以将两个位置相邻的字母平移到某个相邻位置均没有字母的位置,试用最少次数的操作使得所有 \(0\) 的位置均小于 \(1\) 的位置。
\(3 \leq n \leq 100\)
可以发现一次操作最多使得相邻元素相等的对数 \(x\) 增加 \(2\) ,并且第一次操作只会令 \(x\) 至多增加 \(1\) ,而最终状态中 \(x = 2n - 2\) ,因此操作次数下界为 \(n\) 。
尝试达到这个下界,考虑如下构造(以 \(n = 8\) 为例):
OOBABABABABABABABA
ABBABABABABABABOOA
ABBAOOBABABABABBAA
此时中间的部分就是 \(n' = n - 4\) 的子问题,预处理 \(n = 3, 4, 5, 6\) 的情况,满足处理完后序列后面留下两个空位,处理完之后考虑将外面的部分按如下方式处理:
ABBAAAAABBBBBBOOAA
AOOAAAAABBBBBBBBAA
AAAAAAAABBBBBBBBOO
即可完成构造。
#include <bits/stdc++.h>
using namespace std;
int n;
void solve(int l, int r) {
if (r - l + 1 >= 16) {
#define p(a, b) printf("%d to %d\n", a, b)
p(r - 2, l - 2), p(l + 2, r - 2);
solve(l + 4, r - 4);
p(l - 1, r - 5), p(r - 1, l - 1);
#undef p
} else {
#define p(a, b) printf("%d to %d\n", l + a, l + b)
if (r - l + 1 == 6)
p(1, -2), p(4, 1), p(2, -4);
else if (r - l + 1 == 8)
p(5, -2), p(2, 5), p(-1, 2), p(6, -1);
else if (r - l + 1 == 10)
p(7, -2), p(2, 7), p(5, 2), p(-1, 5), p(8, -1);
else if (r - l + 1 == 12)
p(9, -2), p(6, 9), p(1, 6), p(5, 1), p(-1, 5), p(10, -1);
else
p(7, -2), p(4, 7), p(11, 4), p(2, 11), p(8, 2), p(-1, 8), p(12, -1);
#undef p
}
}
signed main() {
scanf("%d", &n);
solve(1, n * 2);
return 0;
}
P9837 汪了个汪
有 \(n\) 行格子,第 \(i\) 行有 \(i\) 个格子,每行的格子严格居中摆放。试将每个格子填入 \(1 \sim n\) 之间的整数,满足:
- 每一行的第一个数互不相同。
- 每一行内所有数互不相同。
- 由任意一行内相邻两个数组成的无序二元组互不相同。
\(n \leq 4000\)
先考虑 \(n\) 为奇数的情况,\(n = 5\) 时可以构造如下方案:

不难发现红线右下部分是一个类似于 \(n = 3\) 的子问题,但是斜线摆放的相对大小是反过来的。而第一条斜线是 \(1 \sim 5\) 递增排列,第二条斜线是 \(n, 1\) 的交错排列,因此可以递归子问题求解。
下面考虑偶数的情况,由于 \(n\) 为奇数时最后一条斜线互不相同,因此只要将最后一条斜线全都放 \(n\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e3 + 7;
vector<int> vec[N];
int n, tp;
void solve(int x, int op) {
if (x == (n + 1) / 2)
vec[n].emplace_back(x);
else {
solve(x + 1, op ^ 1);
for (int i = x * 2; i <= n; ++i)
vec[i].emplace_back((i & 1) ^ op ? n - x + 1 : x);
for (int i = x * 2 - 1; i <= n; ++i)
vec[i].emplace_back(op ? i - x + 1 : n + x - i);
}
}
signed main() {
scanf("%d%d", &n, &tp);
if (n & 1) {
solve(1, 1);
for (int i = 1; i <= n; ++i) {
for (int j = i - 1; ~j; --j)
printf("%d ", vec[i][j]);
puts("");
}
} else {
--n, solve(1, 1);
for (int i = 1; i <= n + 1; ++i) {
for (int j = i - 2; ~j; --j)
printf("%d ", vec[i - 1][j]);
printf("%d\n", n + 1);
}
}
return 0;
}
P12402 [COI 2025] 贪腐 / Korupcija
给定序列 \(a_{0 \sim n - 1}\) ,保证 \(\sum_{i = 0}^{n - 1} a_i = 2^{n - 1}\) 。
构造一组 \(0 \sim 2^n - 1\) 两两配对的方案,要求 \(2^{n - 1}\) 个结果中 \(2^i\) 出现 \(a_i\) 次,或报告无解。
\(n \leq 20\)
按 \(x\) 位分开,则除了 \(a_x\) 对配对外,剩余配对的第 \(x\) 位均相同,因此左右可以递归 \(2^{n - 1} - a_x\) 个数的子问题,显然 \(n > 1\) 时需要满足 \(\forall i, 2 \mid a_i\) 。
考虑递归子问题,若 \(a_{0 \sim n - 2}\) 都是 \(4\) 的倍数,则递归 \(b_i = \frac{a_i}{2}\) 即可。处理 \(n - 1\) 位时找到形如 \((x, x + 2^{n - 2}), (x + 2^{n - 1}, x + 2^{n - 2} + 2^{n - 1})\) 的匹配调整即可。
再考虑存在 \(a_i \bmod 4 = 2\) 的情况,考虑给 \(b_i\) 加上 \(1\) ,最后把一对 \(2^i\) 的匹配调整为 \(2^{n - 1}\) 的匹配即可。此时需要满足 \(\frac{a_{n - 1}}{2} > \sum_{i = 0}^{n - 2} [a_i \bmod 4 = 2]\) 则可以调整,不难发现此时取最大的 \(a_i\) 作为 \(a_{n - 1}\) ,则只有 \(n \leq 6\) 时无法调整,暴搜即可。
#include <bits/stdc++.h>
using namespace std;
int f[1 << 20];
void solve(vector<int> a) {
int n = a.size();
function<bool(int)> dfs = [&](int x) {
if (x == (1 << n))
return true;
if (~f[x])
return dfs(x + 1);
for (int i = 0; i < n; ++i)
if (a[i] && f[x ^ (1 << i)] == -1) {
f[x] = f[x ^ (1 << i)] = i, --a[i];
if (dfs(x + 1))
return true;
f[x] = f[x ^ (1 << i)] = -1, ++a[i];
}
return false;
};
if (a.size() <= 6) {
memset(f, -1, sizeof(int) << a.size()), dfs(0);
return;
}
int x = max_element(a.begin(), a.end()) - a.begin();
swap(a[x], a[n - 1]);
vector<int> b(n - 1), w(n - 1);
int cnt = 0, m = 1 << (n - 1);
for (int i = 0; i < n - 1; ++i)
cnt += (w[i] = a[i] >> 1 & 1);
w[n - 2] += a[n - 1] / 2 - cnt;
for (int i = 0; i < n - 1; ++i)
b[i] = a[i] / 2 + w[i];
solve(b);
for (int i = 0; i < m; ++i)
if ((i >> f[i] & 1) && w[f[i]])
--w[f[i]], f[i] = f[i ^ (1 << f[i])] = n - 1;
for (int i = 0; i < m; ++i)
f[i + m] = f[i];
for (int i = 0; i < (1 << n); ++i)
if ((i >> x & 1) ^ (i >> (n - 1) & 1)) {
int j = i ^ (1 << x | m);
if (i < j)
swap(f[i], f[j]);
}
for (int i = 0; i < (1 << n); ++i)
if (f[i] == x || f[i] == n - 1)
f[i] ^= x ^ (n - 1);
}
signed main() {
int n;
scanf("%d", &n);
if (n == 1)
return puts("0 1"), 0;
vector<int> a(n);
for (int &it : a) {
scanf("%d", &it);
if (it & 1)
return puts("-1"), 0;
}
solve(a);
for (int i = 0; i < (1 << n); ++i)
if (i >> f[i] & 1)
printf("%d %d\n", i, i ^ (1 << f[i]));
return 0;
}
找上下界
CF1685C Bring Balance
给出一个长 \(2n\) 的序列,由 \(n\) 个左括号和 \(n\) 个右括号组成。一次操作可以翻转一个区间,求使其变成合法括号序列的最小操作次数并给出方案。
\(n \leq 10^5\)
画出折线图,则对 \([l + 1, r]\) 进行一次操作相当于将 \([l, r]\) 之间的折线关于 \((\frac{l + r}{2}, \frac{s_l + s_r}{2})\) 中心对称。可能还要上下平移一下,这并不重要。
手玩可以发现答案不超过二,具体有如下讨论:
- 无需操作:此时序列合法。
- 操作一次:记 \(l, r\) 为最左和最右前缀和小于 \(0\) 的点,则操作区间必须包含 \([l, r]\) 。不难发现 \(l, r\) 所在位置越高越好,于是只要判断对 \([1, l]\) 和 \([r, 2n]\) 之间的最高点操作是否合法即可。
- 操作两次:找到最大前缀和 \(s_x\) ,翻转 \([1, x]\) 与 \([x + 1, 2n]\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
int s[N];
char str[N];
int n;
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%s", &n, str + 1);
int l = -1, r = -1;
for (int i = 1; i <= n * 2; ++i) {
s[i] = s[i - 1] + (str[i] == '(' ? 1 : -1);
if (s[i] < 0) {
if (l == -1)
l = i;
r = i;
}
}
if (l == -1 && r == -1) {
puts("0");
continue;
}
int mxl = max_element(s, s + l + 1) - s, mxr = max_element(s + r, s + n * 2 + 1) - s,
mx = max_element(s + l, s + r + 1) - s;
if (s[mxl] + s[mxr] - s[mx] >= 0)
printf("1\n%d %d\n", mxl + 1, mxr);
else {
mx = max_element(s + 1, s + n * 2 + 1) - s;
printf("2\n1 %d\n%d %d\n", mx, mx + 1, n * 2);
}
}
return 0;
}
CF1667C Half Queen Cover
给定一个 \(n \times n\) 的棋盘,求至少需要多少个半皇后,才能使棋盘上每个格子都被攻击到,并给出构造方案。
注:一个点 \((c, d)\) 能够被处于 \((a, b)\) 的半皇后攻击到,当且仅当 \(a = c\) 或 \(b = d\) 或 \(a - b = c - d\) 。
\(n \leq 10^5\)
设答案为 \(k\) ,考虑拿出前 \(k\) 行和前 \(k\) 列使得它们被 \(k\) 个半皇后横竖攻击到。对于剩下 \((n - k) \times (n - k)\) 的区域考虑斜着攻击,共 \(2(n - k) - 1\) 条对角线,因此 \(k \geq 2(n - k) - 1\) ,解得 \(k \geq \lceil \frac{2n - 1}{3} \rceil\) 。
接下来考虑构造方案使得达到这个下界,手动构造可以发现先放一个半皇后在 \((1, 1)\) ,然后每次将 \(x\) 坐标 \(+1\) 、\(y\) 坐标 \(+2\) 即可得到一组构造,需要注意 \(y\) 加出范围的情况,令 \(y \gets 2\) 即可。
#include <bits/stdc++.h>
using namespace std;
signed main() {
int n, k;
scanf("%d", &n);
printf("%d\n", k = (n * 2 + 1) / 3);
for (int i = 1, j = 1; i <= k; ++i, j = (j + 2 > k ? 2 : j + 2))
printf("%d %d\n", i, j);
return 0;
}
调整法
[ARC172D] Distance Ranking
\(n\) 维空间中有 \(n\) 个点,每一维坐标都是 \([0, 10^8]\) 的整数,给出 \(\frac{n(n - 1)}{2}\) 对距离的大小关系,保证没有两对点对距离相等,构造一组合法解。
\(n \leq 20\)
先考虑所有点距离相等的情况,不难构造:
先不考虑坐标为整数的限制,考虑微调每个点的坐标:
则:
考虑简化构造,令 \(\epsilon_{i, i} = 0\) ,且剩下的 \(\epsilon < 1\) ,这样就可以省略二次项来估值得到:
于是令 \(\epsilon_{j, i} = 0\) ,对 \(\epsilon_{i, j}\) 按排名构造即可。由于要求坐标是整数,直接令每个坐标扩大 \(10^8\) 倍即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e1 + 7;
int a[N][N];
int n;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
a[i][i] = 1e8;
for (int i = 1; i <= n * (n - 1) / 2; ++i) {
int x, y;
scanf("%d%d", &x, &y);
a[x][y] = n * (n - 1) / 2 - i + 1;
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j)
printf("%d ", a[i][j]);
puts("");
}
return 0;
}
[AGC061D] Almost Multiplication Table
给定 \(n \times m\) 的矩阵 \(a\) ,其元素均为整数。构造值域在 \([1, 2 \times 10^9]\) 的单增整数数列 \(x_{1 \sim n}\) 与 \(y_{1 \sim m}\) ,最小化 \(\max |a_{i, j} - x_i y_j|\) 。
\(n, m \leq 5\) ,\(a_{i, j} \leq 10^9\)
二分答案 \(k\) ,记 \(l_{i, j} = \max(1, a_{i, j} - k), r_{i, j} = \min(2 \times 10^9, a_{i, j} + k)\) ,则 \(l_{i, j} \leq x_i y_j \leq r_{i, j}\) 。
不妨钦定 \(x_n \leq y_m\) ,\(x_n > y_m\) 的情况类似再做一遍即可。
考虑调整法,从 \(x_i = 1, y_i = 2 \times 10^9\) 开始调整:
- 从小到大调整 \(x\) :\(x_i \gets \max(x_{i - 1} + 1, \lceil \frac{l_{i, j}}{y_j} \rceil)\) 。
- 从大到小调整 \(y\) :\(y_j \gets \min(y_{j + 1} - 1, \lfloor \frac{r_{i, j}}{x_i} \rfloor)\) 。
若 \(x_n > y_m\) 或 \(y_1 < 1\) 则无解。
时间复杂度:由于 \(x_n \leq y_m\) 且 \(x_n y_m \leq r_{n, m}\) 且 \(x_i\) 单增,故 \(\sum x_i\) 为 \(O(n \sqrt{V})\) 级别,而每次调整至少改变一个 \(x_i\) ,故总复杂度为 \(O(nm (n + m) \sqrt{V} \log V)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 2e9;
const int N = 7;
ll L[N][N], R[N][N], a[N][N], x[N], y[N];
int n, m;
inline bool judge() {
for (int i = 1; i <= n; ++i)
if (x[i] < 1 || x[i] > inf)
return false;
for (int i = 1; i <= m; ++i)
if (y[i] < 1 || y[i] > inf)
return false;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (x[i] * y[j] < L[i][j] || x[i] * y[j] > R[i][j])
return false;
return true;
}
inline bool check(ll k) {
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
L[i][j] = max(1ll, a[i][j] - k), R[i][j] = min(inf, a[i][j] + k);
fill(x + 1, x + n + 1, 1), fill(y + 1, y + m + 1, inf);
x[0] = 0, y[m + 1] = inf + 1;
while (!judge() && x[n] <= y[m] && y[1] >= 1) {
for (int i = 1; i <= n; ++i) {
x[i] = max(x[i], x[i - 1] + 1);
for (int j = 1; j <= m; ++j)
x[i] = max(x[i], (L[i][j] + y[j] - 1) / y[j]);
}
for (int j = m; j; --j) {
y[j] = min(y[j], y[j + 1] - 1);
for (int i = 1; i <= n; ++i)
y[j] = min(y[j], R[i][j] / x[i]);
}
}
if (judge())
return true;
fill(y + 1, y + m + 1, 1), fill(x + 1, x + n + 1, inf);
y[0] = 0, x[n + 1] = inf + 1;
while (!judge() && y[m] <= x[n] && x[1] >= 1) {
for (int j = 1; j <= m; ++j) {
y[j] = max(y[j], y[j - 1] + 1);
for (int i = 1; i <= n; ++i)
y[j] = max(y[j], (L[i][j] + x[i] - 1) / x[i]);
}
for (int i = n; i; --i) {
x[i] = min(x[i], x[i + 1] - 1);
for (int j = 1; j <= m; ++j)
x[i] = min(x[i], R[i][j] / y[j]);
}
}
return judge();
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
scanf("%lld", a[i] + j);
ll l = 0, r = inf, ans = inf;
while (l <= r) {
ll mid = (l + r) >> 1;
if (check(mid))
ans = mid, r = mid - 1;
else
l = mid + 1;
}
printf("%lld\n", ans);
check(ans);
for (int i = 1; i <= n; ++i)
printf("%lld ", x[i]);
puts("");
for (int i = 1; i <= m; ++i)
printf("%lld ", y[i]);
return 0;
}
CF1311E Construct the Binary Tree
给定 \(n, d\) ,构造一棵 \(n\) 个点的二叉树,满足所有点的深度和为 \(d\) ,或报告无解。
\(2 \leq n, d \leq 5000\)
可以发现:
- 距离之和最小的是完全二叉树。
- 距离之和最大的是链。
考虑从完全二叉树向链调整。每次选出一个不在链上的叶子 \(x\) ,设链底的深度为 \(d_1\) ,该叶子的深度为 \(d_2\) ,则把 \(x\) 接到链底会让总深度增加 \(d_1 - d_2 + 1\) 。若剩余深度 \(\geq d_1 - d_2 + 1\) 则可以如此做,否则不难求出其应当挂在这条链的哪个位置。
时间复杂度 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 7;
int fa[N], dep[N];
int n, d;
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &d);
dep[0] = -1;
int p = 1;
for (int i = 1; i <= n; ++i) {
d -= (dep[i] = dep[fa[i] = i >> 1] + 1);
if (!(i & (i - 1)))
p = i;
}
if (d < 0) {
puts("NO");
continue;
}
if (d) {
for (int i = n; i; --i) {
if (!(i & (i - 1)))
continue;
d -= dep[p] + 1 - dep[i];
if (d <= 0) {
while (d)
p = fa[p], ++d;
fa[i] = p;
break;
}
dep[i] = dep[fa[i] = p] + 1, p = i;
}
}
if (d)
puts("NO");
else {
puts("YES");
for (int i = 2; i <= n; ++i)
printf("%d ", fa[i]);
puts("");
}
}
return 0;
}
CF1770H Koxia, Mahiru and Winter Festival
有一个 \(n \times n\) 的无向网格图,给定两个 \(1 \sim n\) 的排列 \(p, q\) ,构造 \(2n\) 条路径:
- 对于 \(1 \leq i \leq n\) ,第 \(i\) 条路径起点为 \((1, i)\) ,终点为 \((n, p_i)\) 。
- 对于 \(n + 1 \leq i \leq 2n\) ,第 \(i\) 条路径起点为 \((i, 1)\) ,终点为 \((q_i, n)\) 。
最小化每条边被路径覆盖次数的最大值。
\(n \leq 200\)
先考虑最好情况,即 \(p_i = q_i = i\) ,此时答案为 \(1\) ,且只有这种情况答案为 \(1\) ,证明与构造不难。
再考虑最坏情况,即 \(p_i = q_i = n - i + 1\) ,发现此时可以构造出答案为 \(2\) 的方案。
对于一般情况,考虑从最坏情况调整而来。注意到若 \(p_i > p_{i + 1}\) ,则这两条路径一定有交,只要在交点处交换路径,就相当于交换了 \(p_i\) 和 \(p_{i + 1}\) 。
时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e2 + 7;
vector<pair<int, int> > ans[N << 1];
int a[N], b[N], vis[N][N];
int n;
inline vector<pair<int, int> > solve(int *a) {
vector<pair<int, int> > vec;
for (int i = 1; i <= n; ++i) {
int p = find(a + i, a + n + 1, n - i + 1) - a;
swap(a[i], a[p]), vec.emplace_back(i, p);
}
reverse(vec.begin(), vec.end());
return vec;
}
inline void maintain(vector<pair<int, int> > &a) {
vector<pair<int, int> > b;
for (auto it : a) {
if (vis[it.first][it.second]) {
while (b.size() > vis[it.first][it.second])
vis[b.back().first][b.back().second] = 0, b.pop_back();
} else
b.emplace_back(it), vis[it.first][it.second] = b.size();
}
a = b;
for (auto it : a)
vis[it.first][it.second] = 0;
}
inline void update(vector<pair<int, int> > &a, vector<pair<int, int> > &b) {
for (int i = 0; i < a.size(); ++i)
vis[a[i].first][a[i].second] = i + 1;
for (int i = 0; i < b.size(); ++i) {
if (!vis[b[i].first][b[i].second])
continue;
int x = vis[b[i].first][b[i].second], y = i + 1;
vector<pair<int, int> > ar(a.begin() + x, a.end()), br(b.begin() + y, b.end());
a.resize(x), a.insert(a.end(), br.begin(), br.end());
b.resize(y), b.insert(b.end(), ar.begin(), ar.end());
break;
}
for (auto it : a)
vis[it.first][it.second] = 0;
for (auto it : b)
vis[it.first][it.second] = 0;
maintain(a), maintain(b);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i <= n; ++i)
scanf("%d", b + i);
if (is_sorted(a + 1, a + n + 1) && is_sorted(b + 1, b + n + 1)) {
for (int i = 1; i <= n; ++i) {
printf("%d ", n);
for (int j = 1; j <= n; ++j)
printf("%d %d ", j, i);
puts("");
}
for (int i = 1; i <= n; ++i) {
printf("%d ", n);
for (int j = 1; j <= n; ++j)
printf("%d %d ", i, j);
puts("");
}
return 0;
}
for (int i = 1; i <= n; ++i) {
if (i <= n / 2) { // (1, i) -> (i, i) -> (i, n - i + 1) -> (n, n - i + 1)
for (int j = 1; j <= i; ++j)
ans[i].emplace_back(j, i);
for (int j = i + 1; j <= n - i + 1; ++j)
ans[i].emplace_back(i, j);
for (int j = i + 1; j <= n; ++j)
ans[i].emplace_back(j, n - i + 1);
} else { // (1, i) -> (n - i + 1, i) -> (n - i + 1, n - i + 1) -> (n, n - i + 1)
for (int j = 1; j <= n - i + 1; ++j)
ans[i].emplace_back(j, i);
for (int j = i - 1; j >= n - i + 1; --j)
ans[i].emplace_back(n - i + 1, j);
for (int j = n - i + 2; j <= n; ++j)
ans[i].emplace_back(j, n - i + 1);
}
}
for (int i = 1; i <= n; ++i) { // (i, 1) -> (i, i) -> (n - i + 1, i) -> (n - i + 1, n)
for (int j = 1; j <= i; ++j)
ans[n + i].emplace_back(i, j);
if (i <= n / 2) {
for (int j = i + 1; j <= n - i + 1; ++j)
ans[n + i].emplace_back(j, i);
} else {
for (int j = i - 1; j >= n - i + 1; --j)
ans[n + i].emplace_back(j, i);
}
for (int j = i + 1; j <= n; ++j)
ans[n + i].emplace_back(n - i + 1, j);
}
auto p = solve(a), q = solve(b);
for (auto it : p)
update(ans[it.first], ans[it.second]);
for (auto it : q)
update(ans[n + it.first], ans[n + it.second]);
for (int i = 1; i <= n * 2; ++i) {
printf("%d ", ans[i].size());
for (auto it : ans[i])
printf("%d %d ", it.first, it.second);
puts("");
}
return 0;
}
随机化
UOJ75. 【UR #6】智商锁
给定 \(k\) ,构造一个点数 \(\leq 100\) 的无向图,满足其生成树数量 \(\bmod 998244353 = k\) 。
\(0 \leq k < 998244353\)
考虑随机化,随机 \(1000\) 张点数为 \(12\) 的图,每张图中每条边都以 \(0.8\) 的概率出现,求出每个图的生成树数量 \(f_i\) 。查询时尝试找到 \(k \equiv f_a \times f_b \times f_c \times f_d \pmod{998244353}\) ,这显然可以 meet-in-meddle 处理,构造图就是用桥把它们连起来。
分析一下正确率,将可能的 \(1000^4 = 10^{12}\) 种结果视为均匀分布的整数,则一个数被覆盖的概率为 \(1 - (\frac{998244352}{998244353})^{10^{12}} \approx 1 - e^{-10^3}\) ,近似可以认为所有数都被覆盖的概率为 \((1 - e^{-10^3})^{10^9}\) ,该值已经十分近似于 \(1\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e3 + 7, M = 12;
map<int, pair<int, int> > mp;
int e[N][M][M], g[M][M], ans[N];
mt19937 myrand(time(0));
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
inline int Gauss(int g[M][M], int n) {
int res = 1;
for (int i = 0; i < n; ++i) {
if (!g[i][i]) {
for (int j = i + 1; j < n; ++j)
if (g[j][i]) {
swap(g[i], g[j]), res = dec(0, res);
break;
}
}
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1, inv = mi(g[i][i], Mod - 2); j < n; ++j) {
int div = 1ll * g[j][i] * inv % Mod;
for (int k = i; k < n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
signed main() {
for (int i = 1; i <= 1e3; ++i) {
memset(g, 0, sizeof(g));
for (int j = 0; j < 12; ++j)
for (int k = j + 1; k < 12; ++k)
if (myrand() % 5) {
e[i][j][k] = e[i][k][j] = 1;
g[j][k] = add(g[j][k], 1), g[k][j] = add(g[k][j], 1);
g[j][j] = dec(g[j][j], 1), g[k][k] = dec(g[k][k], 1);
}
ans[i] = Gauss(g, 11);
}
for (int i = 1; i <= 1e3; ++i)
for (int j = 1; j <= 1e3; ++j)
mp[1ll * ans[i] * ans[j] % Mod] = make_pair(i, j);
int T;
scanf("%d", &T);
while (T--) {
int k;
scanf("%d", &k);
if (!k) {
puts("3 1\n1 2");
continue;
}
for (auto it : mp) {
auto it2 = mp.find(1ll * k * mi(it.first, Mod - 2) % Mod);
if (it2 == mp.end())
continue;
int a = it.second.first, b = it.second.second, c = it2->second.first, d = it2->second.second, m = 3;
for (int i = 0; i < 12; ++i)
for (int j = i + 1; j < 12; ++j)
m += e[a][i][j] + e[b][i][j] + e[c][i][j] + e[d][i][j];
printf("48 %d\n", m);
for (int i = 0; i < 12; ++i)
for (int j = i + 1; j < 12; ++j) {
if (e[a][i][j])
printf("%d %d\n", i + 1, j + 1);
if (e[b][i][j])
printf("%d %d\n", 12 + i + 1, 12 + j + 1);
if (e[c][i][j])
printf("%d %d\n", 24 + i + 1, 24 + j + 1);
if (e[d][i][j])
printf("%d %d\n", 36 + i + 1, 36 + j + 1);
}
puts("1 13\n13 25\n25 37");
break;
}
}
return 0;
}
图上结构
P5361 [SDOI2019] 热闹的聚会与尴尬的聚会
给出一个无向图,求:
- 一个子图,记其最小度数为 \(p\) 。
- 一个独立集,记其大小为 \(q\) 。
需要满足 \(\lfloor \frac{n}{q + 1} \rfloor \leq p\) ,且 \(\lfloor \frac{n}{p + 1} \rfloor \leq q\) 。
\(n \leq 10^4\) ,\(m \leq 10^5\)
条件可以转化为 \(pq + p + q \geq n\) ,显然只要让 \(p, q\) 尽量大即可。由于 \(p, q\) 独立,所以可以分开考虑。
先考虑求 \(p\) 的最大值,增量枚举每次删去度数不满足条件的点并判定即可。
再考虑求 \(q\) ,而一般图最大独立集是做不了的。考虑一个近似算法,每次选一个度数最小的点扔入独立集中。
下面考虑该做法是否满足题目的限制。由于每次选出度数最小的点加入独立集,除去这个点本身外,最多会从图中删掉 \(p\) 个点(这可以从第一问得到),因此 \(pq + q \geq n\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int deg[N], tag[N];
int n, m;
inline void solve1() {
priority_queue<pair<int, int> > q;
for (int i = 1; i <= n; ++i)
deg[i] = G.e[i].size(), q.emplace(-deg[i], i), tag[i] = 0;
memset(tag + 1, 0, sizeof(int) * n);
for (int i = 1; !q.empty(); ++i) {
while (!q.empty() && -q.top().first < i) {
auto c = q.top();
q.pop();
if (-c.first != deg[c.second])
continue;
int u = c.second;
tag[u] = i;
for (int v : G.e[u])
if (!tag[v])
--deg[v], q.emplace(-deg[v], v);
}
}
int ansp = *max_element(tag + 1, tag + n + 1);
printf("%d ", count(tag + 1, tag + n + 1, ansp));
for (int i = 1; i <= n; ++i)
if (tag[i] == ansp)
printf("%d ", i);
puts("");
}
inline void solve2() {
priority_queue<pair<int, int> > q;
vector<int> ansq;
for (int i = 1; i <= n; ++i)
deg[i] = G.e[i].size(), q.emplace(-deg[i], i), tag[i] = 0;
while (!q.empty()) {
auto c = q.top();
q.pop();
if (tag[c.second] || -c.first != deg[c.second])
continue;
int u = c.second;
tag[u] = true, ansq.emplace_back(u);
for (int v : G.e[u]) {
if (tag[v])
continue;
tag[v] = true;
for (int w : G.e[v])
if (!tag[w])
--deg[w], q.emplace(-deg[w], w);
}
}
printf("%d ", ansq.size());
for (int it : ansq)
printf("%d ", it);
puts("");
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
G.clear(n);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
solve1(), solve2();
}
return 0;
}
CF1325F Ehab's Last Theorem
给定一张 \(n\) 个点 \(m\) 条边的无向连通图,选择一个任务完成:
- 找到一个大小为 \(\lceil \sqrt{n} \rceil\) 的独立集。
- 找到一个大小至少为 \(\lceil \sqrt{n} \rceil\) 的简单环。
\(5 \leq n \leq 10^5\) ,\(n - 1 \leq m \leq 2 \times 10^5\)
先求出图的 dfs 树,记 \(t = \lceil \sqrt{n} \rceil\) 。
若存在一条跨过至少 \(t\) 个点的返祖边,则完成任务二。
否则将点按深度 \(\bmod (t - 1)\) 分组,则每组都是一个独立集,有抽屉原理至少有一组点数 \(\geq \frac{n}{t - 1}\) ,只要选最大的一组即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<int> vec[N];
int dep[N], sta[N];
bool tag[N];
int n, m, t, top;
bool dfs(int u) {
sta[dep[u] = ++top] = u, vec[dep[u] % (t - 1)].emplace_back(u);
for (int v : G.e[u]) {
if (!dep[v]) {
if (dfs(v))
return true;
} else if (dep[u] - dep[v] + 1 >= t) {
printf("2\n%d\n", dep[u] - dep[v] + 1);
while (sta[top]!= v)
printf("%d ", sta[top--]);
printf("%d\n", v);
return true;
}
}
--top;
return false;
}
signed main() {
scanf("%d%d", &n, &m);
t = ceil(sqrt(n));
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
if (!dfs(1)) {
puts("1");
for (int i = 0; i < t - 1; ++i)
if (vec[i].size() >= t) {
for (int j = 0; j < t; ++j)
printf("%d ", vec[i][j]);
break;
}
}
return 0;
}
CF1364D Ehab's Last Corollary
给定一张 \(n\) 个点 \(m\) 条边的无向连通图和一个整数 \(k\) ,选择一个任务完成:
- 找到一个大小为 \(\lceil \frac{k}{2} \rceil\) 的独立集。
- 找到一个大小不超过 \(k\) 的环。
\(3 \leq k \leq n \leq 10^5\) ,\(n - 1 \leq m \leq 2 \times 10^5\)
先考虑 \(m = n - 1\) 的情况,直接黑白染色找较大的独立集的子集即可。
否则找到深度差最小的返祖边,若其与树边组成的环大小 \(\leq k\) ,则完成任务二,否则隔一个点取一个点即可完成任务一。
因为其深度差最小,所以一定满足独立集的条件。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int fa[N], dep[N], up[N];
int n, m, k;
void dfs(int u, int f) {
fa[u] = f, dep[u] = dep[f] + 1;
for (int v : G.e[u]) {
if (!dep[v])
dfs(v, u);
else if (v != f && dep[v] < dep[u])
up[u] = max(up[u], dep[v]);
}
}
signed main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0);
if (m == n - 1) {
puts("1");
vector<int> vec[2];
for (int i = 1; i <= n; ++i)
vec[dep[i] & 1].emplace_back(i);
for (int i = 0; i < (k + 1) / 2; ++i)
printf("%d ", vec[vec[1].size() >= (k + 1) / 2][i]);
return 0;
}
int cur = -1;
for (int i = 1; i <= n; ++i)
if (up[i] && (cur == -1 || dep[i] - up[i] < dep[cur] - up[cur]))
cur = i;
if (dep[cur] - up[cur] + 1 <= k) {
int len = dep[cur] - up[cur] + 1;
printf("2\n%d\n", len);
for (int i = 1; i <= len; ++i, cur = fa[cur])
printf("%d ", cur);
} else {
puts("1");
for (int i = 1; i <= (k + 1) / 2; ++i, cur = fa[fa[cur]])
printf("%d ", cur);
}
return 0;
}
[AGC032C] Three Circuits
给定一张 \(n\) 个点 \(m\) 条边的简单连通无向图,问能否把图的边集划分为三个环(可以重复经过点)。
\(n, m \leq 10^5\)
由于所有点都在环上,因此每个点的度数都是偶数,即整个图是一张欧拉图。
大力分类讨论:
- 若有一个点度数不小于 \(6\) ,显然可以将欧拉回路按这个点分为三段环。
- 若所有点度数均为 \(2\) ,则整个图是一个环,显然无解。
- 若只有一个点度数为 \(4\) ,其它点度数均为 \(2\) ,则此时只可能是两个环有一个公共点的情况,仍然无解。
- 若有两个点度数为 \(4\) ,其它点度数均为 \(2\) ,分类讨论:
- 若图为两个点之间连着 \(4\) 条链,则无解。
- 若图为两个点之间连着 \(2\) 条链,两个点各挂着一个环,则有解。
- 若至少有三个点度数为 \(4\) ,则一定有解,设其中三个点为 \(A, B, C\) 。首先从点 \(A\) 开始连出两个回路,覆盖所有边,然后分类讨论:
- 若存在一个点 \(B\) 被一个回路访问两次,那么就可以把这个回路拆为两条回路,由于还有一个度数为 \(4\) 的点 \(C\) ,只要在 \(C\) 处拆分回路即可凑出解。
- 否则一定存在点 \(B, C\) 被两个回路恰好访问一次,此时可以拆环为 \(A \to B \to A\) ,\(B \to C \to B\) ,\(A \to C \to A\) ,形成三条回路。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int vis[N];
int n, m, A, B;
void dfs(int u) {
vis[u] = 1;
for (int v : G.e[u]) {
if (vis[v] == 2) {
if (A)
B = v;
else
A = v;
} else if (!vis[v])
dfs(v);
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
int mxdeg = 0, mxcnt = 0;
for (int i = 1; i <= n; ++i) {
if (G.e[i].size() & 1)
return puts("No"), 0;
else if (G.e[i].size() > mxdeg)
mxdeg = G.e[i].size(), mxcnt = 1;
else if (G.e[i].size() == mxdeg)
++mxcnt;
}
if (mxdeg >= 6)
return puts("Yes"), 0;
else if (mxdeg == 2)
return puts("No"), 0;
else if (mxcnt == 1) // mxdeg == 4
return puts("No"), 0;
else if (mxcnt >= 3) // mxdeg == 4
return puts("Yes"), 0;
int cnt = 0;
for (int i = 1; i <= n; ++i)
if (G.e[i].size() == 4)
vis[i] = 2;
for (int i = 1; i <= n; ++i)
if (!vis[i])
A = B = 0, dfs(i), cnt += A == B;
puts(cnt ? "Yes" : "No");
return 0;
}
CF1477D Nezzar and Hidden Permutations
给定一张无向图,将其定向为 DAG,然后选取其两个拓扑序,最大化两个拓扑序种点不同的位置数量。
\(n, m \leq 5 \times 10^5\)
考虑一个特殊情况,若某个点的度数为 \(n - 1\) ,则其排名固定,因此可以删去所有度数为 \(n - 1\) 的点,此时不存在度数为剩余点数 \(- 1\) 的点,即补图一定没有孤立点,记剩余点数为 \(n'\) 。猜测最大点不同位置数量为 \(n'\) ,事实上可以构造达到。
注意到若补图中有一个菊花,那么这个菊花里所有点的排名就都可以改变。具体地,拓扑序中可以先出现中心点再出现其他点,也可以先出现其他点再出现中心点。
考虑将所有点划分到若干个不交的菊花中。对于某个连通块,找出它的一棵生成树,然后在生成树上 bfs。对于遍历到的每一个点:
- 若父亲是菊花中心:直接将当前点划分到父亲所在的菊花中。
- 若当前点不是叶子:直接令其作为菊花中心即可。
- 若当前点是叶子:
- 若父亲所在的菊花大小为 \(2\) ,就将菊花中心移动到父亲,再将当前点划分到该菊花中。
- 若父亲所在的菊花大小至少为 \(3\) ,就将其父亲从它所在的菊花中取出来,并将父亲设置为新的菊花中心,然后将当前点划分到父亲所在的菊花中。
显然这一过程可以将整棵生成树划分为若干个不交的菊花,任意为菊花之间确定两个拓扑序对应位置均不同的拓扑序即可。
找生成树直接在剩余的点中暴力判断,若没有边则连生成树即可,由于每条边只会被遍历一次,使用 set
维护当前剩余的点即可做到 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
set<int> e[N], flower[N], st;
int ans1[N], ans2[N], fa[N], bel[N], rt[N];
int n, m;
void dfs(int u) {
for (int v : st)
if (e[u].find(v) == e[u].end())
G.insert(u, v), fa[v] = u;
for (int v : G.e[u])
st.erase(v);
for (int v : G.e[u])
dfs(v);
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
e[i].clear();
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
e[u].emplace(v), e[v].emplace(u);
}
st.clear();
int cnt = 0;
for (int i = 1; i <= n; ++i) {
if (e[i].size() == n - 1)
ans1[i] = ans2[i] = ++cnt;
else
st.emplace(i);
}
G.clear(n);
int tot = 0;
for (int i = 1; i <= n; ++i) {
if (st.find(i) == st.end())
continue;
st.erase(i), fa[i] = 0, dfs(i);
queue<int> q;
q.emplace(i);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : G.e[u])
q.emplace(v);
if (fa[u] && fa[u] == rt[bel[fa[u]]])
flower[bel[u] = bel[fa[u]]].emplace(u);
else if (!G.e[u].empty())
flower[bel[u] = ++tot] = {u}, rt[tot] = u;
else if (flower[bel[fa[u]]].size() == 2)
rt[bel[fa[u]]] = fa[u], flower[bel[u] = bel[fa[u]]].emplace(u);
else {
flower[bel[fa[u]]].erase(fa[u]), rt[++tot] = fa[u];
flower[bel[u] = bel[fa[u]] = tot] = {u, fa[u]};
}
}
}
for (int i = 1; i <= tot; ++i) {
ans1[rt[i]] = ++cnt;
for (int it : flower[i])
if (it != rt[i])
ans2[it] = cnt, ans1[it] = ++cnt;
ans2[rt[i]] = cnt;
}
for (int i = 1; i <= n; ++i)
printf("%d ", ans1[i]);
puts("");
for (int i = 1; i <= n; ++i)
printf("%d ", ans2[i]);
puts("");
}
return 0;
}
图的构造
P11337 「COI 2019」IZLET
给出 \(n \times n\) 的矩阵 \(a\) ,其中 \(a_{i, j}\) 表示 \(i \to j\) 路径上出现的颜色种数。
构造一棵合法的树,每个点赋上 \(1 \sim n\) 的颜色,保证有解。
\(n \leq 3 \times 10^3\)
不难发现 \(a_{i, j} = 1\) 当且仅当 \(i, j\) 属于树上一个同色极大连通块。考虑用并查集提取出所有同色极大连通块,每个同色极大连通块都可以选出一个关键点,将其他点连成以关键点为根的菊花,于是只要考虑关键点之间的连边。
考虑关键点构成的树,此时相邻两点异色,因此 \(a_{i, j} = 2\) 的两点一定通过边连接,可以建出树的结构。
考虑确定这些关键点的颜色,记 \(u, v\) 路径上离 \(u\) 最近的点为 \(x\) ,离 \(v\) 最近的点为 \(y\) ,则当且仅当 \(a_{u, v} = a_{u, y} = a_{v, x} = a_{x, y} + 1\) 时 \(u, v\) 同色且路径上没有与之颜色相同的点。所以直接从每个点开始 dfs 一遍,连接这些边即可。
时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu1, dsu2;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int a[N][N], col[N];
int testid, n;
void dfs(int u, int x, int y, int v) {
if (x != v && a[u][v] == a[u][y] && a[u][v] == a[v][x] && a[u][v] == a[x][y] + 1)
dsu2.merge(u, v);
for (int nxt : G.e[u])
if (nxt != x)
dfs(nxt, u, y, v);
}
signed main() {
scanf("%d%d", &testid, &n);
dsu1.prework(n);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
scanf("%d", a[i] + j);
if (a[i][j] == 1)
dsu1.merge(i, j);
}
vector<pair<int, int> > edg;
vector<int> id;
for (int i = 1; i <= n; ++i) {
if (dsu1.find(i) == i)
id.emplace_back(i);
else
edg.emplace_back(i, dsu1.find(i));
}
dsu2.prework(n);
for (int u : id)
for (int v : id)
if (a[u][v] == 2 && dsu2.find(u) != dsu2.find(v))
G.insert(u, v), G.insert(v, u), edg.emplace_back(u, v), dsu2.merge(u, v);
dsu2.prework(n);
for (int u : id)
for (int v : G.e[u])
dfs(u, v, u, v);
int tot = 0;
for (int u : id)
if (dsu2.find(u) == u)
col[u] = ++tot;
for (int u : id)
col[u] = col[dsu2.find(u)];
for (int u = 1; u <= n; ++u)
if (dsu1.find(u) != u)
col[u] = col[dsu1.find(u)];
for (int i = 1; i <= n; ++i)
printf("%d ", col[i]);
puts("");
for (auto it : edg)
printf("%d %d\n", it.first, it.second);
return 0;
}
P7816 「Stoi2029」以父之名
给出一张无向图,每条边的边权为 \(1\) 或 \(2\) ,构造一组边的定向满足每个点的入边权值和与出边权值和相差 \(1\) 。
\(n \leq 10^6\) ,\(m \leq 3 \times 10^6\) ,保证有解
由于保证有解,因此一个点所连的边权和为奇数,而且一个很好的性质是只要最小化每个点的权值差即可做到权值差为 \(1\) 。
考虑欧拉回路模型。新建一个虚点,向所有奇度点连权值为 \(1\) 的边。由于奇度点的数量为偶数,因此新图中所有点均为偶度点,存在欧拉回路,由此可以给边定向。
跑欧拉回路时,若通过一条边权为 \(w\) 的边到达 \(u\) ,则优先选取边权为 \(w\) 的边出去,若不存在则选边权不为 \(w\) 的边。
正确性:由于每个点所连的边权和均为奇数,即连了奇数条 \(1\) 边,而其与虚点有连边当且仅当 \(2\) 边有偶数条。因此,若是从一条 \(2\) 边进入,则一定存在一条 \(2\) 边出来;若是从一条 \(1\) 边进入,则由于虚边只会被走一次,因此若没有 \(1\) 的出边则会让权值差增加 \(1\) 。而这种情况只有一次,因此合法。
时间复杂度线性。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, M = 6e6 + 7;
struct Graph {
struct Edge {
int nxt, v, w;
} e[M];
int head[N];
int tot;
inline void insert(int u, int v, int w) {
e[++tot] = (Edge){head[u], v, w}, head[u] = tot;
}
} G[2];
int deg[N], ans[M];
bool vis[N];
int n, m;
void dfs(int u, int w) {
vis[u] = true;
auto solve = [&](int op) {
for (int &i = G[op].head[u]; i; i = G[op].e[i].nxt) {
int v = G[op].e[i].v, id = G[op].e[i].w;
if (~ans[abs(id)])
continue;
ans[abs(id)] = id < 0, dfs(v, op);
}
};
solve(w), solve(w ^ 1);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G[w & 1].insert(u, v, i), G[w & 1].insert(v, u, -i);
++deg[u], ++deg[v];
}
for (int i = 1; i <= n; ++i)
if (deg[i] & 1)
G[1].insert(i, n + 1, m + i), G[1].insert(n + 1, i, m + i);
memset(ans + 1, -1, sizeof(int) * (m + n));
for (int i = 1; i <= n; ++i)
if (!vis[i])
dfs(i, 0);
for (int i = 1; i <= m; ++i)
putchar(ans[i] | '0');
return 0;
}
序列操作
P7999 [WFOI - 01] 翻转序列(requese)
给出一个 \(1 \sim n\) 的排列,需要在一开始选定一个数 \(x\) ,然后每次操作可以翻转长度为 \(x - 1\) 或长度为 \(x + 1\) 的子段,构造一组操作数 \(\leq 20n\) 的方案将排列排序。
\(n \leq 1000\)
首先注意到 \(x\) 必须是奇数,因为如果是偶数,则操作无法改变位置的奇偶性。
对于一个 \(a_v = u (u \leq v)\) ,首先先用若干次 \(x + 1\) 的操作将 \(v\) 不断左移 \(x\) 格,一直移动到 \(v \in [u, u + x)\) 。此时若 \(v = u\) 就结束了,否则考虑将 \(v\) 移动到 \(u + x\) 后交换 \(u, u + x\) 使其归位。
对 \(u, v\) 的奇偶性进行讨论:
- 若 \(u, v\) 奇偶性相同,则操作 \((\frac{v + u + x}{2} - \frac{x + 1}{2} + 1, \frac{v + u + x}{2} + \frac{x + 1}{2})\) 即可。
- 若 \(u, v\) 奇偶性不同,则用一次操作将 \(v\) 移动到 \(v + x\) 的位置,之后就和上面一样了。
将 \(1 \sim \lfloor \frac{n}{2} \rfloor\) 排好序后,\(\lfloor \frac{n}{2} \rfloor \sim n\) 需要从 \(n\) 开始倒着处理
注意到该方法后面至少需要预留 \(2x\) 的位置,因此 \(x \leq \frac{n}{4}\) 。
但是还有些细节,处理 \(\lfloor \frac{n}{2} \rfloor \sim n\) 时不能把前半部分打乱,考虑特殊处理。
首先第一个部分(若干次移动 \(x\) 格)的操作显然不会打乱前半部分,然后考虑后面的处理。处理方式是简单的,归位后操作一次 \((v + 1, v + x - 1)\) 的位置,这样最后一步操作可以视为仅交换 \(v, v + x\) ,然后剩下的操作是不会涉及到 \(v\) 的,因此不断回退(倒序重复原来的操作)即可。
该方法的操作次数上界为 \(n \times \frac{n}{x} + 3 \times \frac{n}{2} + 6 \times \frac{n}{2}\) ,因此 \(x\) 取 \(\leq \frac{n}{4}\) 的最大奇数即可做到操作数上界为 \(8.5 n\) 。
注意该方法不能处理 \(n \leq 4\) 的情况,这一部分暴力做就好了。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
vector<pair<int, int> > ans;
int a[N];
int n;
inline void update(int l, int r) {
reverse(a + l, a + r + 1), ans.emplace_back(l, r);
}
inline void output() {
printf("%d\n", (int)ans.size());
for (auto it : ans)
printf("%d %d\n", it.first, it.second);
}
inline void BruteForce() {
puts("3");
for (int i = 1; i <= n && !is_sorted(a + 1, a + n + 1); ++i)
for (int j = 1; j < n; ++j)
if (a[j] > a[j + 1])
update(j, j + 1);
output();
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
if (n <= 4)
return BruteForce(), 0;
int x = n / 4;
if (~x & 1)
--x;
printf("%d\n", x);
int half = (x + 1) / 2;
for (int u = 1; u <= n / 2; ++u) {
int v = u;
for (; v && a[v] != u; ++v);
for (; v - x >= u; v -= x)
update(v - x, v);
if (v == u)
continue;
if ((u & 1) != (v & 1))
update(v, v + x), v += x;
update((v + u + x) / 2 - half + 1, (v + u + x) / 2 + half), update(u, u + x);
}
for (int u = n; u > n / 2; --u) {
int v = u;
for (; v && a[v] != u; --v);
for (; v + x <= u; v += x)
update(v, v + x);
if (v == u)
continue;
int r = u - x;
bool flag = false;
if ((u & 1) != (v & 1))
update(v - x, v), v -= x, flag = true;
update((v + r) / 2 - half + 1, (v + r) / 2 + half);
update(r, r + x), update(r + 1, r + x - 1);
update((v + r) / 2 - half + 1, (v + r) / 2 + half);
if (flag)
update(v, v + x);
}
output();
return 0;
}
构造序列
P3599 Koishi Loves Construction
给定 \(n\) :
Task 1:构造一个 \(1 \sim n\) 的排列满足 \(n\) 个前缀和模 \(n\) 互异,或报告无解。
Task 2:构造一个 \(1 \sim n\) 的排列满足 \(n\) 个前缀积模 \(n\) 互异,或报告无解。
\(n \leq 10^5\)
先考虑 Task 1 ,注意到 \(n\) 必须排在开头,否则 \(+n\) 后前缀和不变。当 \(n\) 为奇数时,若 \(n > 1\) ,则由于 \(s_n = \frac{n(n - 1)}{2} \bmod n = 0 = s_1\) ,无解。否则可以构造 \(s_{1 \sim n} = \{ 0, 1, -1, 2, -2, 3, -3, \cdots \}\) 满足 \(s_i\) 互异,易得此时 \(a_i = \begin{cases} n - i + 1 & 2 \mid i \\ i - 1 & 2 \nmid i \end{cases}\) 。
接下来考虑 Task 2,同样可以注意到:
- \(n\) 必须排在末尾,否则 \(\times n\) 后前缀积均为 \(0\) 。
- \(1\) 必须排在开头,否则 \(\times 1\) 后前缀积不变。
- \(n\) 为大于 \(4\) 的合数时无解,因为 \(s_{n - 1} = (n - 1)! \bmod n = s_n = 0\) ,注意 \(n = 4 = 2 \times 2\) 时可以构造出一组合法解 \(\{ 1, 3, 2, 4 \}\) 。
先特判掉 \(n = 1\) 与 \(n = 4\) 的情况,接下来考虑对素数 \(n\) 构造 \(s_{1 \sim n} = \{ 1, 2, 3, \cdots, 0 \}\) 的序列,可以得到 \(s_i \equiv s_{i - 1} + 1 \equiv s_{i - 1} \times a_i\) ,解得 \(a_i = \frac{1}{s_{i - 1}} + 1 = (i - 1)^{-1} + 1\) ,显然满足题设。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int inv[N];
inline bool isprime(int n) {
for (int i = 2; i * i <= n; ++i)
if (!(n % i))
return false;
return true;
}
inline void solve1(int n) {
if (n == 1)
puts("2 1");
else if (n & 1)
puts("0");
else {
printf("2 %d ", n);
for (int i = 2; i <= n; ++i)
printf("%d ", i & 1 ? i - 1 : n - i + 1);
puts("");
}
}
inline void solve2(int n) {
if (n == 1)
puts("2 1");
else if (n == 4)
puts("2 1 3 2 4");
else if (isprime(n)) {
inv[0] = inv[1] = 1;
for (int i = 2; i < n; ++i)
inv[i] = 1ll * (n - n / i) * inv[n % i] % n;
printf("2 1 ");
for (int i = 2; i < n; ++i)
printf("%d ", inv[i - 1] + 1);
printf("%d\n", n);
} else
puts("0");
}
signed main() {
int op, T;
scanf("%d%d", &op, &T);
while (T--) {
int n;
scanf("%d", &n);
if (op == 1)
solve1(n);
else
solve2(n);
}
return 0;
}
P8098 [USACO22JAN] Tests for Haybales G
给出不降序列 \(j_{1 \sim n}\) ,选取一个整数 \(k \in [1, 10^{18}]\) 并构造一个不降序列 \(a_{1 \sim n} \in[0, 10^{18}]\) ,满足对于任意 \(i\) 均有 \(j_i\) 为最大满足 \(a_{j_i} \leq a_i + k\) 的位置。
\(n \leq 10^5\)
考虑建树,连边 \(i \to j_i + 1\) ,则构建出一棵以 \(n + 1\) 为根的树。则条件变为:每个点至少比儿子大 \(k\) 、同一层编号大的儿子更大。
对于后一个条件,只要按 dfs 序微调即可,任取一个足够大的 \(k\) ,而前一个条件只要按深度构造即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int dep[N], dfn[N];
int n, dfstime;
void dfs(int u) {
dfn[u] = ++dfstime, reverse(G.e[u].begin(), G.e[u].end());
for (int v : G.e[u])
dep[v] = dep[u] + 1, dfs(v);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int j;
scanf("%d", &j);
G.insert(j + 1, i);
}
dfs(n + 1);
printf("%d\n", n * 2);
for (int i = 1; i <= n; ++i)
printf("%lld\n", 2ll * n * (n * 2 - dep[i]) - dfn[i]);
return 0;
}
其他
CF1270G Subset with Zero Sum
给定序列 \(a_{1 \sim n}\) ,满足 \(i - n \leq a_i \leq i - 1\) ,找到一个和为 \(0\) 的非空子集。
\(n \leq 10^6\)
条件等价于 \(1 \leq i - a_i \leq n\) ,考虑建图,连 \(n\) 条形如 \(i \to i - a_i\) 的边,找环即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
struct Graph {
vector<int> e[N];
int indeg[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear(), indeg[i] = 0;
}
inline void insert(int u, int v) {
e[u].emplace_back(v), ++indeg[v];
}
} G;
int n;
inline void TopoSort() {
queue<int> q;
for (int i = 1; i <= n; ++i)
if (!G.indeg[i])
q.emplace(i);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : G.e[u]) {
--G.indeg[v];
if (!G.indeg[v])
q.emplace(v);
}
}
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n), G.clear(n);
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
G.insert(i, i - x);
}
TopoSort();
printf("%d\n", n - (int)count(G.indeg + 1, G.indeg + n + 1, 0));
for (int i = 1; i <= n; ++i)
if (G.indeg[i])
printf("%d ", i);
puts("");
}
return 0;
}
P8477 「GLR-R3」春分
有一个原电池装置,可以通过挡板将其分为两个部分,两部分的溶液通过导线连接。
现有两组溶液 \(X = \{ x_1, x_2, \cdots, x_n \}, Y = \{ y_1, y_2, \cdots, y_n \}\) ,这 \(2n\) 种溶液两两不同,需要将其两两搭配完成 \(n^2\) 组实验。
导线数量无限,每次实验都可以用新的导线。而挡板数量是有限的,定义挡板的污染:
- 若某一面接触溶液,则会沾上该溶液产生污染。
- 若某一面接触被污染面,则会沾上被污染面的所有种溶液产生污染,两个接触面的沾上的溶液均为原先两个接触面的溶液的并集。
关于挡板的使用方法有如下说明:
- 可以一次将若干挡板紧贴着插入,组合为一个大挡板使用。
- 挡板分左右两面,挡板可以翻转左右面。
实验中任意档板沾上任何一种溶液的一侧都不能和非同种的溶液接触。
求最小挡板数量 \(m\) ,并给出实验方案。
\(n \leq 609\) ,\(m \leq 712\)
一个比较简单的方法是分配 \(m = 2n\) 个板,每次两两组合实验。容易发现该方案不优的地方在于中间还有 \(2n\) 个干净面是残余的,并没有得到利用。
事实上一个板如果只脏了一面,还可以换一面给其它溶液配上。
直观上能够感知到,为了减小挡板数量,一个有效的方式是多次将挡板的污染面接触,将若干挡板组合使用。
考虑一个分组的思想,将 \(X, Y\) 的挡板混合使用,从而减少挡板数量。
考虑将 \(X, Y\) 平均分为 \(X_{1 \sim 3}, Y_{1 \sim 3}\) ,给 \(X_1, Y_{1 \sim 3}\) 分配挡板,记为 \(A_1, B_{1 \sim 3}\) 。进行如下反应:

发现该方案不优的地方在于需要用一大片右面干净板来保证 \(B\) 板的左面干净,而实际上该操作可以通过一个额外板解决。
考虑将 \(X, Y\) 分组,将 \(X, Y\) 平均分为 \(X_{1 \sim 2}, Y_{1 \sim 3}\) ,给 \(X_1, Y_{1 \sim 2}\) 分配挡板,记为 \(A_1, B_{1 \sim 2}\) 。申请一个额外板 \(C\) ,进行如下反应:

即可通过。
#include <bits/stdc++.h>
using namespace std;
int n;
signed main() {
scanf("%d", &n);
int a = (n + 1) / 2, b = (n + 2) / 3, c = (n - b + 1) / 2, m = a + b + c + 1;
printf("%d\n", m);
for (int i = 1; i <= a; ++i)
for (int j = 1; j <= b + c; ++j)
printf("2 %d %d %d %d\n", i, i, a + j, j);
for (int i = 1; i <= a; ++i)
for (int j = 1; j <= n - b - c; ++j)
printf("3 %d %d %d %d %d\n", i, i, m, -(a + b + j), b + c + j);
for (int i = 1; i <= n - a; ++i)
for (int j = 1; j <= b; ++j)
printf("3 %d %d %d %d %d\n", a + i, -i, -m, a + j, j);
for (int i = 1; i <= n - a; ++i)
for (int j = 1; j <= c; ++j)
printf("2 %d %d %d %d\n", a + i, -i, -(a + j), b + j);
for (int i = 1; i <= n - a; ++i)
for (int j = 1; j <= n - b - c; ++j)
printf("2 %d %d %d %d\n", a + i, -i, -(a + b + j), b + c + j);
return 0;
}
P9195 [JOI Open 2016] JOIRIS
有一个特殊的俄罗斯方块游戏,游戏在一个宽为 \(n\) 、高度无限的平面中进行。
最初第 \(i\) 列的最下方 \(a_i\) 个格子上有方块,其他位置没有。在每一回合,将进行如下事件:
- 玩家可以决定一个大小为 \(1 \times k\) 的板块的下落方向和位置,方向为水平或者垂直。
- 决定好下落的方向后,板块将从无穷高处落下,直到无法下落,即其下方紧邻已有方块。
- 如果此时有行被方块完全覆盖,则该行被消去,并且这一行上面的所有方块下落一格。
- 如果此时平面内没有任何方块,则游戏结束。
构造一组方案在有限回合内结束游戏,或报告无解。
\(k \leq n \leq 50\) ,\(0 \leq a_i \leq 50\)
下标从 \(0\) 开始,记 \(b_i = (\sum_{j \bmod k = i} a_j) \bmod k\) 。
先考虑放骨牌的影响:
- 横放:会使每个 \(b_i\) 变为 \((b_i + 1) \bmod k\) 。
- 竖放:\(b_i\) 不变。
再考虑消除一行的影响,发现 \(\lbrack 0, n \bmod k)\) 和 \(\lbrack n \bmod k, k)\) 这两个区间里的 \(b_i\) 的相对大小不会发生改变。因此有解必要条件是 \(b_0 = b_1 = \cdots = b_{(n \bmod k) - 1}\) 且 \(b_{n \bmod k} = b_{(n \bmod k) + 1} = \cdots = b_{k - 1}\)。
下面考虑构造方案。
-
首先添加若干竖着的骨牌,使得每列高度从左到右不降。
-
然后通过横着平铺,从下到上依次尽可能填满每一行,右边的空格优先填满。
-
此时 \(\lbrack k - 1, n)\) 列高度完全相等,因此可以不断往 \([0, k - 2]\) 列插入竖骨牌,完全删除 \(\lbrack k - 1, n)\) 。
-
由于 \(b_{n \bmod k} = b_{(n \bmod k) + 1} = \cdots = b_{k - 1} = 0\) ,因此这些列的高度是 \(k\) 的倍数,那么可以直接插入若干个竖骨牌将这些列删空。
-
此时仅 \(\lbrack 0, n \bmod k)\) 列不为空,由于 \(b_0 = b_1 = \cdots = b_{(n \bmod k) - 1}\) ,那么可以先将左边补齐到同一高度,然后在右边横着铺满即可。
可能需要特殊处理 \(k = 1\) 的情况,这是简单的。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e1 + 7;
vector<pair<int, int> > ans;
int a[N], b[N];
int n, k;
inline void update(int x, int y) {
ans.emplace_back(x, y + 1);
if (x == 1)
a[y] += k;
else {
for (int i = 0; i < k; ++i)
++a[y + i];
}
for (int i = 0, mn = *min_element(a, a + k); i < n; ++i)
a[i] -= mn;
}
inline void output() {
printf("%d\n", ans.size());
for (auto it : ans)
printf("%d %d\n", it.first, it.second);
}
signed main() {
scanf("%d%d", &n, &k);
for (int i = 0; i < n; ++i)
scanf("%d", a + i), b[i % k] = (b[i % k] + a[i]) % k;
if (k == 1) {
int mx = *max_element(a, a + n);
for (int i = 0; i < n; ++i)
for (int j = a[i]; j < mx; ++j)
ans.emplace_back(1, i + 1);
output();
return 0;
}
for (int i = 1; i < k; ++i)
if (i != n % k && b[i - 1] != b[i])
return puts("-1"), 0;
for (int i = 1; i < n; ++i)
while (a[i] < a[i - 1])
update(1, i); // 添加若干竖着的骨牌,使得骨牌数量从左到右单调不降
for (int i = 1; i < n - 1; ++i) {
int delta = a[i + 1] - a[i];
for (int j = i - k + 1; j >= 0; j -= k)
for (int l = 0; l < delta; ++l)
update(2, j); // 横着平铺,从下到上依次填满每一行
}
for (int i = 0; i <= k - 2; ++i)
for (int j = 0; j <= i; ++j)
for (int delta = a[n - 1] - a[j]; delta > 0; delta -= k)
update(1, j); // 不断向 [0, k - 2] 内插入竖着的骨牌以完全删除 [k - 1, n)
int mx = *max_element(a + n % k, a + k);
for (int i = 0; i < n; ++i)
for (int delta = mx - a[i]; delta > 0; delta -= k)
update(1, i); // 消掉 [n % k, k - 1]
mx = *max_element(a, a + n % k);
for (int i = 0; i < n % k; ++i)
while (a[i] < mx)
update(1, i); // 补齐左边
for (int i = n % k; i < n; i += k)
for (int j = 0; j < mx; ++j)
update(2, i); // 补齐右边
output();
return 0;
}
QOJ9841. Elegant Tetris
有一个 \(20 \times m\) 的容器,行的编号自底向上为 \(1 \sim 20\) 。方块积木有七种:
每次操作可选择一个积木从空中的某个位置旋转某个角度后下落,直到碰到容器底部或其他积木后停止,在下落的过程中不能移动或旋转积木。
若在这个积木停止后,存在一行的每一列都有积木,那么这一行将被消去,上方的所有方块将会整体向下平移一格。
若消除了所有可以消除的行后,仍有某个积木的一部分高于容器顶部,则会输掉游戏。
给定容器内方块的初始状态(注意初始状态方块可以悬空),构造一种不超过 \(10^4\) 次操作的方案,在不输掉游戏的前提下让最终容器内的状态和开始时完全相同。
\(m \leq 10^3\) ,最高的方块高度 \(\leq 16\)
考虑找到最顶上的⼀个方块,然后在这个方块的上面构造整行。
首先放上⼀个倒 T
作为地基,再在左右分别用 S
和 Z
扩展,接着在左侧放⼀个竖 T
,再在上层用 Z
铺满,最后再用一个 L
(宽度奇数)或两个 T
(宽度偶数)处理右侧的空余,如下图所示:
其中当宽为偶数且最顶上方块在最右列时,需要将构造方案左右翻转⼀下。
#include <bits/stdc++.h>
using namespace std;
const int M = 1e3 + 7;
struct Node {
char op;
int x, y;
inline Node(char _op, int _x, int _y) : op(_op), x(_x), y(_y) {}
};
vector<Node> ans;
char str[M];
int n, m;
signed main() {
scanf("%d%d", &m, &n);
if (m == 1) {
puts("1");
puts("I 0 1");
return 0;
} else if (m == 2) {
puts("1");
puts("O 0 1");
return 0;
} else if (m == 3) {
puts("3");
puts("T 2 1");
puts("T 1 2");
puts("J 2 1");
return 0;
}
int pos = 0;
for (int i = 1; i <= n; ++i) {
scanf("%s", str + 1);
for (int j = 1; j <= m; ++j)
if (str[j] == '#') {
pos = j;
break;
}
if (pos)
break;
}
if (!pos)
pos = 1;
if (m & 1) {
if (pos == m)
pos -= 2;
if (~pos & 1)
--pos;
ans.emplace_back('T', 2, pos);
for (int i = pos + 2; i + 2 <= m; i += 2)
ans.emplace_back('Z', 0, i);
for (int i = pos - 2; i >= 1; i -= 2)
ans.emplace_back('S', 0, i);
ans.emplace_back('T', 3, 1);
for (int i = 2; i + 3 <= m; i += 2)
ans.emplace_back('Z', 0, i);
ans.emplace_back('L', 2, m - 1);
} else {
if (~pos & 1)
--pos;
if (pos == m - 1) {
ans.emplace_back('T', 2, m - 2);
for (int i = m - 4; i >= 2; i -= 2)
ans.emplace_back('S', 0, i);
ans.emplace_back('T', 1, m - 1);
ans.emplace_back('T', 3, 1);
for (int i = m - 3; i >= 2; i -= 2)
ans.emplace_back('S', 0, i);
ans.emplace_back('T', 0, 1);
} else {
ans.emplace_back('T', 2, pos);
for (int i = pos + 2; i + 3 <= m; i += 2)
ans.emplace_back('Z', 0, i);
ans.emplace_back('T', 1, m - 1);
for (int i = pos - 2; i >= 1; i -= 2)
ans.emplace_back('S', 0, i);
ans.emplace_back('T', 3, 1);
for (int i = 2; i + 4 <= m; i += 2)
ans.emplace_back('Z', 0, i);
ans.emplace_back('T', 0, m - 2);
}
}
printf("%d\n", ans.size());
for (Node it : ans)
printf("%c %d %d\n", it.op, it.x, it.y);
return 0;
}
P8520 [IOI 2021] 喷泉公园
给定平面上的 \(n\) 个点 \((x_i, y_i)\) ,其中每个点的坐标均为偶数。
需要在这些点之间连边使得图连通,两点之间可以连边当且仅当距离为 \(2\) 。
之后需要给每条边指定一个点 \((a_j, b_j)\) ,要求其到两端点的距离均为 \(\sqrt{2}\) ,且所有 \((a_j, b_j)\) 互不相同。
构造一组方案,或报告无解。
\(n \leq 2 \times 10^5\)
首先若 \(n\) 个点无法连通则一定无解,猜测连通则一定有解。
考虑将 \((a_j, b_j)\) 看作 \([a_j - 1, a_j + 1] \times [b_j - 1, b_j + 1]\) 的格子。对平面上的格子黑白染色,黑色的只连横着的边,白色的只连竖着的边。
考虑从上到下,从左到右加入所有边,并尽量保证我们当前的图与所有边都加入的图连通性相同,黑色块上面的边和白色块左面的边一定能加入,只需要考虑两种情况:
- 黑色块下面的边:此时上、左、右三条边已经(可能以等价形式)加入,因此这条边无需加入。
- 白色块右面的边:此时上、左两条边已经加入,而下面的边可以连黑色的块,因此这条边无需加入。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
struct Point {
int x, y;
inline bool operator < (const Point &rhs) const {
return y == rhs.y ? x < rhs.x : y > rhs.y;
}
} p[N];
void build(vector<int> u, vector<int> v, vector<int> a, vector<int> b);
int construct_roads(vector<int> x, vector<int> y) {
int n = x.size();
map<Point, int> mp;
for (int i = 1; i <= n; ++i)
mp[p[i] = (Point){x[i - 1], y[i - 1]}] = i;
sort(p + 1, p + n + 1), dsu.prework(n);
vector<int> U, V, A, B;
set<Point> st;
for (int i = 1; i <= n; ++i) {
int x = p[i].x, y = p[i].y;
if (mp.find((Point){x + 2, y}) != mp.end()) {
int u = mp[(Point){x, y}], v = mp[(Point){x + 2, y}];
if (dsu.find(u) != dsu.find(v)) {
int a = x + 1, b = y + 1;
if (((a + b) / 2) & 1)
b -= 2;
if (st.find((Point){a, b}) != st.end())
continue;
st.insert((Point){a, b}), dsu.merge(v, u);
U.emplace_back(u - 1), V.emplace_back(v - 1), A.emplace_back(a), B.emplace_back(b);
}
}
if (mp.find((Point){x, y - 2}) != mp.end()) {
int u = mp[(Point){x, y}], v = mp[(Point){x, y - 2}];
if (dsu.find(u) != dsu.find(v)) {
int a = x + 1, b = y - 1;
if (~((a + b) / 2) & 1)
a -= 2;
if (st.find((Point){a, b}) != st.end())
continue;
st.insert((Point){a, b}), dsu.merge(v, u);
U.emplace_back(u - 1), V.emplace_back(v - 1), A.emplace_back(a), B.emplace_back(b);
}
}
}
for (int i = 2; i <= n; ++i)
if (dsu.find(i) != dsu.find(1))
return 0;
return build(U, V, A, B), 1;
}
P12417 基础构造练习题 1
有 \(n\) 个实数,每个数未知。定义一次操作为:选择两个数,将其同时变为它们的乘积。
构造一组方案,满足任意情况下操作后所有数相等。
\(n \leq 1024\) ,操作次数上界 \(2047\) 次
首先 \(n\) 为奇数时必然无解,因为无论如何操作,最大值数量显然始终为偶数,具体的取每个数为互异的质数即可。
接下来特判 \(n = 2, 4\) 的情况后,考虑将序列对半分为 \(a_{1 \sim \frac{n}{2}}\) 和 \(a_{\frac{n}{2} + 1 \sim n}\) ,考虑如下策略:
- 先用 \(\frac{n}{2} + 2\) 次操作将序列调整为:
- 对于 \(1 \leq i \leq \frac{n}{2} - 2\) ,有 \(a_i = a_{n - 1 - i}\) 。
- \(a_{\frac{n}{2} - 1} = a_{\frac{n}{2}} = a_{n - 1} = a_n = x\) 。
- 再令 \(a_n\) 与 \(a_{1 \sim \frac{n}{2} - 1}\) 和 \(a_{\frac{n}{2} + 1 \sim n - 2}\) 按顺序依次操作,记 \(T = x \prod_{i = 1}^{\frac{n}{2} - 2} a_i\) ,此时序列形如:
- 对于 \(1 \leq i \leq \frac{n}{2} - 2\) ,有 \(a_i = x \prod_{j = 1}^i a_j\) ,\(a_{n - i - 1} = \frac{T^2}{\prod_{j = 1}^{i - 1} a_j}\) 。
- \(a_{\frac{n}{2} - 1} = Tx\) ,\(a_{\frac{n}{2}} = a_{n - 1} = x\) ,\(a_n = T^2\) 。
- 然后对于 \(1 \leq i \leq \frac{n}{2} - 3\) ,将 \((a_i, a_{n - 2 - i})\) 两两匹配,再将 \((a_{\frac{n}{2} - 2}, a_{\frac{n}{2} - 1}), (a_{\frac{n}{2}}, a_{n - 2}), (a_{n - 1}, a_n)\) 两两匹配,这样所有的数就都变成 \(T^2x\) 了。
总操作次数为 \((\frac{n}{2} + 2) + (n - 3) + (\frac{n}{2} - 3 + 3) = 2n - 1\) 次。
#include <bits/stdc++.h>
using namespace std;
vector<pair<int, int> > ans;
signed main() {
int T;
scanf("%d", &T);
while (T--) {
int n;
scanf("%d", &n);
if (n & 1) {
puts("0");
continue;
}
puts("1"), ans.clear();
if (n == 2)
ans.emplace_back(1, 2);
else if (n == 4) {
ans.emplace_back(1, 2), ans.emplace_back(3, 4);
ans.emplace_back(1, 3), ans.emplace_back(2, 4);
} else {
for (int i = 1; i <= n / 2 - 2; ++i)
ans.emplace_back(i, n - 1 - i);
ans.emplace_back(n / 2 - 1, n / 2), ans.emplace_back(n - 1, n);
ans.emplace_back(n / 2 - 1, n - 1), ans.emplace_back(n / 2, n);
for (int i = 1; i <= n / 2 - 1; ++i)
ans.emplace_back(i, n);
for (int i = 1; i <= n / 2 - 2; ++i)
ans.emplace_back(n / 2 + i, n);
for (int i = 1; i <= n / 2 - 3; ++i)
ans.emplace_back(i, n - 2 - i);
ans.emplace_back(n / 2 - 2, n / 2 - 1);
ans.emplace_back(n / 2, n - 2);
ans.emplace_back(n - 1, n);
}
printf("%d\n", (int)ans.size());
for (auto it : ans)
printf("%d %d\n", it.first, it.second);
}
return 0;
}