【集训队作业】IOI 2020 集训队作业 试题泛做 2

Atcoder Grand Contest 25

Problem D. Choosing Points

考虑对于给定点集 S S 和输入 D 1 , D 2 D_1,D_2 ,计算一个大小不小于 S 4 \frac{S}{4} 的独立集 w o r k ( S , D 1 , D 2 ) work(S,D_1,D_2)

对于 D 1 , D 2 D_1,D_2 均为奇数的情况,可以直接对点集二分图染色。

对于 D 1 , D 2 D_1,D_2 存在一个奇数的情况,不妨设为 D 1 D_1 ,可以对点集二分图染色,对得到的两个新点集 A , B A,B 重新计算 w o r k ( A , D 2 ) , w o r k ( B , D 2 ) work(A,D_2),work(B,D_2) ,取较大者。

对于 D 1 , D 2 D_1,D_2 存在一个模 4 4 2 2 的数的情况,不妨设为 D 1 D_1 ,则 D 1 D_1 必定是两个奇数的平方和,同样可以对点集进行条纹染色,对得到的两个新点集 A , B A,B 重新计算 w o r k ( A , D 2 ) , w o r k ( B , D 2 ) work(A,D_2),work(B,D_2) ,取较大者。

否则,即 D 1 , D 2 D_1,D_2 均为 4 4 的倍数,可以将点集分为四份,分别计算 w o r k ( T , D 1 2 , D 2 2 ) work(T,\frac{D_1}{2},\frac{D_2}{2}) ,合并得到的点集即可。

时间复杂度 O ( N 2 L o g N ) O(N^2LogN)

实际上,通过上述过程,我们也可以发现对于单独的 D D ,得到的图是一张二分图,因此,对 D 1 , D 2 D_1,D_2 分别二分图染色,取四份中较大的一份即可。

时间复杂度 O ( N 3 ) O(N^3)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
vector <pair <int, int>> work(vector <pair <int, int>> input, int a) {
	if (input.size() <= 1) return input;
	vector <pair <int, int>> ans;
	if (a % 2) {
		int cnt[2] = {0, 0};
		for (auto x : input) cnt[(x.first + x.second) & 1]++;
		int home = cnt[0] > cnt[1] ? 0 : 1;
		for (auto x : input) if ((x.first + x.second) % 2 == home) ans.push_back(x);
		return ans;
	}
	if (a % 4) {
		int cnt[2] = {0, 0};
		for (auto x : input) cnt[x.first & 1]++;
		int home = cnt[0] > cnt[1] ? 0 : 1;
		for (auto x : input) if (x.first % 2 == home) ans.push_back(x);
		return ans;
	}
	vector <pair <int, int>> nxt, tmp;
	nxt.clear(); for (auto x : input) if (x.first % 2 == 0 && x.second % 2 == 0) nxt.emplace_back(x.first / 2, x.second / 2);
	tmp = work(nxt, a / 4); for (auto x : tmp) ans.emplace_back(x.first * 2, x.second * 2);
	nxt.clear(); for (auto x : input) if (x.first % 2 == 0 && x.second % 2 == 1) nxt.emplace_back(x.first / 2, x.second / 2);
	tmp = work(nxt, a / 4); for (auto x : tmp) ans.emplace_back(x.first * 2, x.second * 2 + 1);
	nxt.clear(); for (auto x : input) if (x.first % 2 == 1 && x.second % 2 == 0) nxt.emplace_back(x.first / 2, x.second / 2);
	tmp = work(nxt, a / 4); for (auto x : tmp) ans.emplace_back(x.first * 2 + 1, x.second * 2);
	nxt.clear(); for (auto x : input) if (x.first % 2 == 1 && x.second % 2 == 1) nxt.emplace_back(x.first / 2, x.second / 2);
	tmp = work(nxt, a / 4); for (auto x : tmp) ans.emplace_back(x.first * 2 + 1, x.second * 2 + 1);
	return ans;
}
vector <pair <int, int>> work(vector <pair <int, int>> input, int a, int b) {
	if (input.size() <= 1) return input;
	vector <pair <int, int>> ans;
	if (a % 2 && b % 2) {
		int cnt[2] = {0, 0};
		for (auto x : input) cnt[(x.first + x.second) & 1]++;
		int home = cnt[0] > cnt[1] ? 0 : 1;
		for (auto x : input) if ((x.first + x.second) % 2 == home) ans.push_back(x);
		return ans;
	}
	if (a % 2) swap(a, b);
	if (b % 2) {
		vector <pair <int, int>> nxt1, nxt2;
		for (auto x : input) {
			if ((x.first + x.second) & 1) nxt1.push_back(x);
			else nxt2.push_back(x);
		}
		vector <pair <int, int>> tmp1 = work(nxt1, a);
		vector <pair <int, int>> tmp2 = work(nxt2, a);
		if (tmp1.size() > tmp2.size()) return tmp1;
		else return tmp2;
	}
	if (a % 4) swap(a, b);
	if (b % 4) {
		vector <pair <int, int>> nxt1, nxt2;
		for (auto x : input) {
			if (x.first & 1) nxt1.push_back(x);
			else nxt2.push_back(x);
		}
		vector <pair <int, int>> tmp1 = work(nxt1, a);
		vector <pair <int, int>> tmp2 = work(nxt2, a);
		if (tmp1.size() > tmp2.size()) return tmp1;
		else return tmp2;
	}
	vector <pair <int, int>> nxt, tmp;
	nxt.clear(); for (auto x : input) if (x.first % 2 == 0 && x.second % 2 == 0) nxt.emplace_back(x.first / 2, x.second / 2);
	tmp = work(nxt, a / 4, b / 4); for (auto x : tmp) ans.emplace_back(x.first * 2, x.second * 2);
	nxt.clear(); for (auto x : input) if (x.first % 2 == 0 && x.second % 2 == 1) nxt.emplace_back(x.first / 2, x.second / 2);
	tmp = work(nxt, a / 4, b / 4); for (auto x : tmp) ans.emplace_back(x.first * 2, x.second * 2 + 1);
	nxt.clear(); for (auto x : input) if (x.first % 2 == 1 && x.second % 2 == 0) nxt.emplace_back(x.first / 2, x.second / 2);
	tmp = work(nxt, a / 4, b / 4); for (auto x : tmp) ans.emplace_back(x.first * 2 + 1, x.second * 2);
	nxt.clear(); for (auto x : input) if (x.first % 2 == 1 && x.second % 2 == 1) nxt.emplace_back(x.first / 2, x.second / 2);
	tmp = work(nxt, a / 4, b / 4); for (auto x : tmp) ans.emplace_back(x.first * 2 + 1, x.second * 2 + 1);
	return ans;
}
int main() {
	int n, a, b; read(n), read(a), read(b);
	vector <pair <int, int>> input;
	for (int i = 0; i < n * 2; i++)
	for (int j = 0; j < n * 2; j++)
		input.emplace_back(i, j);
	vector <pair <int, int>> res = work(input, a, b); int cnt = 0;
	for (auto x : res) if (++cnt <= n * n) printf("%d %d\n", x.first, x.second);
	return 0;
}

Problem E. Walking on a Tree

显然,记 c i c_i 表示第 i i 条边被覆盖的次数,显然答案存在上界 i min { c i , 2 } \sum_{i}\min\{c_i,2\}

考虑构造方案取到该上界,对于叶子节点 i i ,令其父边被覆盖次数为 x x
x 1 x\leq 1 ,显然,可以删去节点 i i

考虑 x 2 x\geq 2 的情况,任取两条覆盖该点的路径 ( a , i ) , ( i , b ) (a,i),(i,b)
则若要使得 i i 被覆盖两次, ( a , i ) , ( i , b ) (a,i),(i,b) 的方向必须不同。
考虑 ( a , i ) , ( i , b ) (a,i),(i,b) 的交 ( i , c ) (i,c) ( i , c ) (i,c) 上的边都被覆盖了两次,在后续过程中都不需要继续考虑。
考虑 ( i , c ) (i,c) 外的部分,可以发现 a c , c b a\rightarrow c,c\rightarrow b 等价于 a b a\rightarrow b ,而 b c , c a b\rightarrow c,c\rightarrow a 等价于 b a b\rightarrow a ,因此可以加入 ( a , b ) (a,b) 代替上述两条路径参与后续计算。

由此,我们构造了一种方案。

时间复杂度 O ( N × ( N + M ) ) O(N\times (N+M))

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e3 + 5;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
vector <int> a[MAXN];
int depth[MAXN], father[MAXN];
int n, m, tot, ans, x[MAXN], y[MAXN];
bool used[MAXN], ok[MAXN], res[MAXN];
int nowx[MAXN], nowy[MAXN], f[MAXN], g[MAXN];
void dfs(int pos, int fa) {
	depth[pos] = depth[fa] + 1;
	father[pos] = fa;
	for (auto x : a[pos])
		if (x != fa) dfs(x, pos);
}
void mark(int a, int b, int c, int d) {
	static int cnt[MAXN];
	memset(cnt, 0, sizeof(cnt));
	while (a != b) {
		if (depth[a] < depth[b]) swap(a, b);
		cnt[a]++, a = father[a];
	}
	while (c != d) {
		if (depth[c] < depth[d]) swap(c, d);
		cnt[c]++, c = father[c];
	}
	for (int i = 1; i <= n; i++)
		ok[i] |= cnt[i] >= 2;
}
void work(int pos, int fa) {
	for (auto x : a[pos])
		if (x != fa) work(x, pos);
	if (pos != 1) {
		int cnt = 0;
		for (int i = 1; i <= tot; i++)
			if (!used[i]) cnt += (nowx[i] == pos) ^ (nowy[i] == pos);
		ans += min(max(cnt, ok[pos] * 2), 2);
		if (cnt <= 1 || ok[pos]) {
			for (int i = 1; i <= tot; i++)
				if (!used[i]) {
					if (nowx[i] == pos) nowx[i] = fa;
					if (nowy[i] == pos) nowy[i] = fa;
				}
			return;
		}
		vector <int> points;
		for (int i = 1; i <= tot; i++)
			if (!used[i] && ((nowx[i] == pos) ^ (nowy[i] == pos))) {
				points.push_back(i);
			}
		int a = points[0], b = points[1];
		mark(nowx[a], nowy[a], nowx[b], nowy[b]);
		used[a] = used[b] = true;
		if (nowx[a] == pos && nowx[b] == pos) {
			tot++, f[tot] = -a, g[tot] = -b;
			x[tot] = y[a], nowx[tot] = nowy[a];
			y[tot] = y[b], nowy[tot] = nowy[b];
		}
		if (nowy[a] == pos && nowx[b] == pos) {
			tot++, f[tot] = a, g[tot] = -b;
			x[tot] = x[a], nowx[tot] = nowx[a];
			y[tot] = y[b], nowy[tot] = nowy[b];
		}
		if (nowx[a] == pos && nowy[b] == pos) {
			tot++, f[tot] = -a, g[tot] = b;
			x[tot] = y[a], nowx[tot] = nowy[a];
			y[tot] = x[b], nowy[tot] = nowx[b];
		}
		if (nowy[a] == pos && nowy[b] == pos) {
			tot++, f[tot] = a, g[tot] = b;
			x[tot] = x[a], nowx[tot] = nowx[a];
			y[tot] = x[b], nowy[tot] = nowx[b];
		}
		for (int i = 1; i <= tot; i++)
			if (!used[i]) {
				if (nowx[i] == pos) nowx[i] = fa;
				if (nowy[i] == pos) nowy[i] = fa;
			}
	}
}
int main() {
	read(n), read(m), tot = m;
	for (int i = 1; i <= n - 1; i++) {
		int x, y; read(x), read(y);
		a[x].push_back(y);
		a[y].push_back(x);
	}
	for (int i = 1; i <= m; i++) {
		read(x[i]), read(y[i]);
		nowx[i] = x[i];
		nowy[i] = y[i];
	}
	dfs(1, 0);
	work(1, 0);
	writeln(ans);
	for (int i = tot; i >= 1; i--) {
		if (!used[i]) res[i] = true;
		if (i > m) {
			int a = abs(f[i]), b = abs(g[i]);
			res[a] = res[i] ^ (x[i] != x[a]);
			res[b] = res[i] ^ (y[i] != y[b]);
		}
	}
	for (int i = 1; i <= m; i++)
		if (res[i]) printf("%d %d\n", x[i], y[i]);
		else printf("%d %d\n", y[i], x[i]);
	return 0;
}

Problem F. Addition and Andition

S i , T i S_i,T_i 表示两个高精度数从最低位开始的第 i i 位。

定义一次对 i i 进位操作如下:
首先,令 S i = T i = 2 S_i=T_i=2
S i = 2 S_i=2 T i = 2 T_i=2 ,将其变为 0 0 ,并对下一位 + 1 +1 ,令 i = i + 1 i=i+1 ,否则,结束进位操作。

定义一次加法操作如下:
从大到小对于每一个 S i = T 1 = 1 S_i=T_1=1 i i ,进行进位操作。

题目要求我们进行 K K 次加法操作。

考虑如下过程,从大到小对于每一个 S i = T 1 = 1 S_i=T_1=1 i i ,进行 K K 次如下操作 ( 1 ) (1)
找到最小的 j i j\geq i ,使得 S j = T j = 1 S_j=T_j=1 ,对 j j 进行进位操作,若不存在这样的 j j ,则停止操作。

这是因为在同一次加法操作中,先进行的进位操作造成的增量将会大于之后进行的所有进位操作造成的增量之和,并且,对 i i 进行进位操作后,有 S i = T i = 0 S_i=T_i=0 ,因此,交换循环的顺序是可行的。

考虑用形式化的语言描述 ( 1 ) (1)
x x 表示当前进位, l f t lft 表示剩余操作次数,初始时,令 S i = T i = 0 S_i=T_i=0 x = 11 , l f t = K x=11,lft=K
对于 j = i , i + 1 , i + 2 , j=i,i+1,i+2,\dots 进行如下操作 ( 2 ) (2)
( S j , T j , x ) = ( 0 , 0 , 10 ) (S_j , T_j , x) = (0, 0, 10) , 令 ( S j , T j ) = ( 1 , 0 ) (S_j , T_j ) = (1, 0) ,结束操作
( S j , T j , x ) = ( 0 , 0 , 01 ) (S_j , T_j , x) = (0, 0, 01) , 令 ( S j , T j ) = ( 0 , 1 ) (S_j , T_j ) = (0, 1) ,结束操作
( S j , T j , x ) = ( 0 , 0 , 11 ) (S_j , T_j , x) = (0, 0, 11) , 令 ( S j , T j , x ) = ( 0 , 0 , 11 ) , l f t = l f t 1 (S_j , T_j , x) = (0, 0, 11), lft = lft - 1 , 若此时 l f t < 0 lft < 0 , 令 ( S j , T j ) = ( 1 , 1 ) (S_j , T_j ) = (1, 1) ,结束操作
( S j , T j , x ) = ( 1 , 0 , 11 ) (S_j , T_j , x) = (1, 0, 11) , 令 ( S j , T j , x ) = ( 0 , 1 , 10 ) (S_j , T_j , x) = (0, 1, 10)
( S j , T j , x ) = ( 1 , 0 , 10 ) (S_j , T_j , x) = (1, 0, 10) , 令 ( S j , T j , x ) = ( 0 , 0 , 10 ) (S_j , T_j , x) = (0, 0, 10)
( S j , T j , x ) = ( 1 , 0 , 01 ) (S_j , T_j , x) = (1, 0, 01) , 令 ( S j , T j , x ) = ( 0 , 0 , 11 ) , l f t = l f t 1 (S_j , T_j , x) = (0, 0, 11), lft = lft - 1 , 若此时 l f t < 0 lft < 0 , 令 ( S j , T j ) = ( 1 , 1 ) (S_j , T_j ) = (1, 1) ,结束操作
可以类似地处理 ( S j , T j ) = ( 0 , 1 ) (S_j , T_j ) = (0, 1) 的情况。

注意到证明循环可以交换的过程实际上证明了我们不会在 ( 2 ) (2) 中遇到 ( S j , T j ) = ( 1 , 1 ) (S_j , T_j ) = (1, 1) 的情况。

观察上述过程,处理大段 ( S j , T j ) = ( 0 , 0 ) (S_j , T_j ) = (0, 0) 是可以做到较为快速的,因此我们可以对 ( S j , T j ) (S_j , T_j ) 分段,整段处理 ( 0 , 0 ) (0,0) 的情况,具体实现时可以用一个栈来维护 ( S j , T j ) ( 0 , 0 ) (S_j , T_j )\ne (0, 0) 的位置。

除了对于 ( 0 , 0 ) (0,0) 的操作以外,剩余操作都将减少 1 1 的个数,因此该算法的时间复杂度是线性的。

时间复杂度 O ( N + M + K ) O(N+M+K)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e6 + 5;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
char s[MAXN];
int n, m, k, a[MAXN], b[MAXN];
int l, resa[MAXN], resb[MAXN];
stack <pair <int, int>> stk;
int main() {
	read(n), read(m), read(k);
	scanf("\n%s", s + 1);
	for (int i = 1, j = n; i <= n; i++, j--)
		a[j] = s[i] - '0';
	scanf("\n%s", s + 1);
	for (int i = 1, j = m; i <= m; i++, j--)
		b[j] = s[i] - '0';
	for (int i = max(n, m); i >= 1; i--) {
		if (a[i] + b[i] == 1) stk.emplace(i, a[i] * 2 + b[i]);
		if (a[i] + b[i] <= 1) continue;
		int lft = k, pos = i, x = 3;
		stack <pair <int, int>> add;
		while (!stk.empty()) {
			pair <int, int> tmp = stk.top();
			if (pos == tmp.first) {
				stk.pop();
				if ((x ^ tmp.second) == 3) {
					x = 3;
					if (lft) lft--;
					else break;
				} else if (x == 3) {
					x &= tmp.second;
					add.emplace(tmp.first, tmp.second ^ 3);
				}
				pos += 1;
			} else if (x == 3) {
				if (tmp.first - pos > lft) break;
				lft -= tmp.first - pos;
				pos = tmp.first;
			} else break;
		}
		if (x == 3) resa[pos + lft] = resb[pos + lft] = 1;
		else add.emplace(pos, x);
		while (!add.empty()) {
			stk.push(add.top());
			add.pop();
		}
	}
	while (!stk.empty()) {
		pair <int, int> tmp = stk.top();
		resa[tmp.first] += (tmp.second >> 1) & 1;
		resb[tmp.first] += (tmp.second) & 1;
		stk.pop();
	}
	l = max(n, m) + k;
	while (resa[l] == 0) l--;
	for (int i = l; i >= 1; i--) putchar(resa[i] + '0');
	putchar('\n');
	l = max(n, m) + k;
	while (resb[l] == 0) l--;
	for (int i = l; i >= 1; i--) putchar(resb[i] + '0');
	putchar('\n');
	return 0;
}

Atcoder Grand Contest 26

Problem D. Histogram Coloring

首先可以考虑删去所有不在任意一个 2 × 2 2\times 2 的正方形中的方格,每删去一个,便将答案 × 2 \times 2

考虑用 2 × 2 2\times 2 的正方形的样式代替 1 × 1 1\times 1 的正方形,来表示剩余部分。

考虑一些显然的性质, KaTeX parse error: Undefined control sequence: \array at position 1: \̲a̲r̲r̲a̲y̲{\array{0,0}\\\… 的上下必然是 KaTeX parse error: Undefined control sequence: \array at position 1: \̲a̲r̲r̲a̲y̲{\array{1,1}\\\…KaTeX parse error: Undefined control sequence: \array at position 1: \̲a̲r̲r̲a̲y̲{\array{1,0}\\\… 的左右必然是 KaTeX parse error: Undefined control sequence: \array at position 1: \̲a̲r̲r̲a̲y̲{\array{0,1}\\\…

因此,我们称存在 KaTeX parse error: Undefined control sequence: \array at position 1: \̲a̲r̲r̲a̲y̲{\array{0,0}\\\… 的列是一个关键的列,考虑计算 d p i dp_i 表示第 i i 列是关键列时,前 i i 列的涂色方式数,可以通过枚举下一个关键列的方式转移。

关于转移系数的计算,可以注意到,对于不存在 KaTeX parse error: Undefined control sequence: \array at position 1: \̲a̲r̲r̲a̲y̲{\array{0,0}\\\…KaTeX parse error: Undefined control sequence: \array at position 1: \̲a̲r̲r̲a̲y̲{\array{1,1}\\\… 的一行,其上方一行的涂色方式恰好有 2 2 种,直接计算可以自由决定的行数 c n t cnt 2 c n t 2^{cnt} 即为转移系数。

时间复杂度 O ( N 3 + N 2 L o g V ) O(N^3+N^2LogV) ,可简单优化至 O ( N 2 ) O(N^2)

官方题解中介绍了一种 O ( N L o g V ) O(NLogV) 的做法,本质上可以看做是用笛卡尔树的思想对该算法的优化。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 105;
const int P = 1e9 + 7;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
int power(int x, ll y) {
	if (y == 0) return 1;
	int tmp = power(x, y / 2);
	if (y % 2 == 0) return 1ll * tmp * tmp % P;
	else return 1ll * tmp * tmp % P * x % P;
}
void update(int &x, int y) {
	x += y;
	if (x >= P) x -= P;
}
ll cnt; int dp[MAXN];
int n, ans, a[MAXN], b[MAXN];
int main() {
	read(n), ans = 1;
	for (int i = 1; i <= n; i++) {
		read(a[i]);
		if (a[i] == 1) {
			cnt += 1;
			a[i] = 0;
		}
	}
	for (int i = 1; i <= n; i++) {
		int Max = max(a[i - 1], a[i + 1]);
		if (a[i] > Max) {
			cnt += a[i] - Max;
			a[i] = Max;
		}
	}
	ans = power(2, cnt);
	for (int i = 1; i <= n - 1; i++) {
		b[i] = min(a[i], a[i + 1]);
		if (b[i]) b[i]--;
	}
	dp[0] = 1;
	for (int i = 0; i <= n - 1; i++) {
		for (int j = i + 1; j <= n; j++) {
			static int c[MAXN];
			memset(c, 0, sizeof(c));
			for (int k = i + 1, Min = min(b[k], b[i]); k <= j - 1; k++, chkmin(Min, b[k]))
				chkmax(c[k], Min);
			for (int k = j - 1, Min = min(b[k], b[j]); k >= i + 1; k--, chkmin(Min, b[k]))
				chkmax(c[k], Min);
			ll cnt = 0;
			for (int k = i + 1; k <= j - 1; k++) {
				c[k] = b[k] - c[k];
				if (c[k] - c[k - 1] > 0) cnt += c[k] - c[k - 1];
			}
			if (b[i] == 0 && (b[j] != 0 || i != j - 1)) cnt++;
			update(dp[j], 1ll * dp[i] * power(2, cnt) % P);
			if (b[j] == 0) break;
		}
	}
	writeln(1ll * ans * dp[n] % P);
	return 0;
}

Problem E. Synchronized Subsequence

将字符串尽可能地分割,使得在保证同一组字符均在每一段中的情况下,字符串被分成的段数尽量多。

注意到我们需要最大化字典序,对于字符串 S + T S+T ,分别最大化 S S T T 的字典序即可最大化 S + T S+T 的字典序,因此,可以考虑分别计算每一段的答案,再通过简单 dp 合并答案。

完成分割后,可以发现,同一段内的每一组字符的先后顺序一定是固定的,否则一定可以细分。

对于先后顺序是 ab 的段,最优的方案显然是贪心地选取尽可能多的 ab​ ,使得不存在连续的 a 。

对于先后顺序是 ba 的段,考虑枚举第一对选的字符 i i ,由分段的性质,可以贪心地发现选取 i i 后的每一对字符得到的结果一定是最优的,因此枚举后比较即可。

时间复杂度 O ( N 2 ) O(N^2)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 6005;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
char s[MAXN];
int n, tot, rk[MAXN];
string res[MAXN], dp[MAXN];
int la, lb, a[MAXN], b[MAXN];
int main() {
	read(n), scanf("\n%s", s + 1);
	for (int i = 1; i <= 2 * n; i++)
		if (s[i] == 'a') a[++la] = i, rk[i] = la;
		else b[++lb] = i, rk[i] = lb;
	int last = 0;
	for (int i = 1; i <= n; i++) {
		if (i == n || max(a[i], b[i]) < min(a[i + 1], b[i + 1])) {
			if (a[i] < b[i]) {
				res[++tot] = ""; int Max = 0;
				for (int j = last + 1; j <= i; j++)
					if (a[j] > Max) {
						res[tot] += "ab";
						Max = b[j];
					}
			} else {
				res[++tot] = "";
				for (int j = last + 1; j <= i; j++) {
					string tmp = "";
					for (int k = last * 2 + 1; k <= i * 2; k++)
						if (rk[k] >= j) tmp += s[k];
					chkmax(res[tot], tmp);
				}
			}
			last = i;
		}
	}
	for (int i = tot; i >= 1; i--)
		dp[i] = max(dp[i + 1], res[i] + dp[i + 1]);
	cout << dp[1] << endl;
	return 0;
}

Problem F. Manju Game

记数列奇数位的数和为 B B ,偶数位的数和为 W W

考虑 N N 为偶数的情况,此时,先手初始时走 1 1 N N 处可以分别保证得到 B B 分和 W W 分,因此先手得分不低于 max ( B , W ) \max(B,W) ;同时后手存在策略将先手得分控制在 max ( B , W ) \max(B,W) 以内,因此 N N 为偶数时先手得分为 max ( B , W ) \max(B,W)

对于 N N 为奇数的情况 ,首先,若先手初始时走 1 1 处,可以保证得到 B B 分,并且,走在其余奇数位置,后手能够保证先手的得分不超过 B B 。因此,先手想要得到多于 B B 分,必须初始时走在偶数位置。

考虑此时游戏的进程:
在先手玩家的回合,先手玩家可能选择得到 B B 分,结束游戏;或者走在某个偶数位置。
此时,后手玩家可以选择该位置的一侧,先手玩家得到该侧的 W W 分,游戏在另一侧继续。

那么,先手玩家的决策可以写作一棵二叉树的形式,区间 [ L , R ] [L,R] 对应的根节点表示先手玩家走在的偶数位置,若不存在,则表示先手玩家选择得到 B B 分,结束游戏。左右子树对应了不同的后手玩家的决策。

不难发现,对于给定的决策树,后手玩家可以决定游戏结束的子树,因此,树的结构并不重要,先手玩家需要最大化所有结束区间的 B W B-W 的最小值。

二分答案后可以通过前缀和简单判断。

时间复杂度 O ( N L o g V ) O(NLogV)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 3e5 + 5;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
int n, w, b, a[MAXN], s[MAXN];
bool check(int mid) {
	int Min = 0;
	for (int i = 2; i <= n; i += 2)
		if (s[i - 1] - Min >= mid) chkmin(Min, s[i]);
	return s[n] - Min >= mid;
}
int main() {
	read(n);
	for (int i = 1; i <= n; i++) {
		read(a[i]);
		if (i & 1) b += a[i], s[i] = s[i - 1] + a[i];
		else w += a[i], s[i] = s[i - 1] - a[i];
	}
	if (n % 2 == 0) {
		printf("%d %d\n", max(w, b), min(w, b));
		return 0;
	}
	int l = -w - b, r = w + b;
	while (l < r) {
		int mid = (l + r + 2 * w + 2 * b + 1) / 2 - w - b;
		if (check(mid)) l = mid;
		else r = mid - 1;
	}
	printf("%d %d\n", w + l, b - l);
	return 0;
}

Atcoder Grand Contest 27

Problem D. Modulo Matrix

将棋盘黑白染色,一种直观的想法是在白点处填入一些互不相同的小质数,在黑点处填入相邻质数的最小公倍数 + 1 +1 ,这样便可以保证满足条件。

现在问题在于对值域的限制约为 O ( ( k N ) 4 ) O((kN)^4) ,若直接填入最小的 O ( N 2 ) O(N^2) 个质数,最小公倍数将会达到 O ( ( k N ) 8 ) O((kN)^8) 级别。

考虑如此构造数组
r a = { , p 1 , p 2 , p 2 , p 3 , p 3 , p 4 , } , r b = { , p 1 p 2 , p 1 p 2 , p 2 p 3 , p 2 p 3 , p 3 p 4 , p 3 p 4 , } ra=\{\dots,p_1,p_2,p_2,p_3,p_3,p_4,\dots\},rb=\{\dots,p_1p_2,p_1p_2,p_2p_3,p_2p_3,p_3p_4,p_3p_4,\dots\}
c a = { , q 1 , q 2 , q 2 , q 3 , q 3 , q 4 , } , c b = { , q 1 q 2 , q 1 q 2 , q 2 q 3 , q 2 q 3 , q 3 q 4 , q 3 q 4 , } ca=\{\dots,q_1,q_2,q_2,q_3,q_3,q_4,\dots\},cb=\{\dots,q_1q_2,q_1q_2,q_2q_3,q_2q_3,q_3q_4,q_3q_4,\dots\}

其中 p , q p,q 是两组互不相同的质数。

则在白点处填入 r a i × c a j ra_i\times ca_j ,在黑点处填入 r b i × c b j + 1 rb_i\times cb_j+1 即可。

如此构造,每个值均可能出现两次,需要对其中一个进行 × 2 \times 2 处理。

时间复杂度 O ( N 2 ) O(N^2)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e4 + 5;
typedef long long ll;
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
int tot, prime[MAXN], f[MAXN];
void sieve(int n) {
	for (int i = 2; i <= n; i++) {
		if (f[i] == 0) prime[++tot] = f[i] = i;
		for (int j = 1; j <= tot && prime[j] <= f[i]; j++) {
			int tmp = prime[j] * i;
			if (tmp > n) break;
			f[tmp] = prime[j];
		}
	}
}
int a[MAXN], b[MAXN];
int main() {
	int n; read(n), sieve(1e4);
	int now = 1;
	for (int i = 0; i <= n + 1; i++) {
		a[i] = prime[now];
		if (i & 1) now++;
	}
	now++;
	for (int i = 0; i <= n + 1; i++) {
		b[i] = prime[now];
		if (i & 1) now++;
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++)
			if ((i + j) & 1) {
				if (i & 1) printf("%lld ", 2ll * a[i - 1] * a[i + 1] * b[j - 1] * b[j + 1] + 1);
				else printf("%lld ", 4ll * a[i - 1] * a[i + 1] * b[j - 1] * b[j + 1] + 1);	
			} else {
				if (i & 1) printf("%lld ", 1ll * a[i] * b[j]);
				else printf("%lld ", 2ll * a[i] * b[j]);
			}
		printf("\n");
	}
	return 0;
}

Problem E. ABBreviate

考虑将 a 看做 + 1 +1 , b 看做 + 2 +2 ,则进行一次操作不会改变序列在模 3 3 意义下的总和。

任何的操作方案的效果都是将若干区间的字符变为了一个单独的字符,考虑能够一段字母 S S 变成一个单独的字符 c c 的条件,应当为
( 1 ) (1) 、模 3 3 意义下 S S c c 的总和相等
( 2 ) (2) S = 1 S=1 S S 中存在相邻的相同字符

满足以上两点是充分且必要的。

考虑枚举最终串 T T ,判断 S S 是否能变为 T T

首先,若 S S 中不存在相邻的相同字符,则当且仅当 S = T S=T S S 能够变为 T T

否则,考虑贪心地将 S S 划分为若干区间 I i I_i ,第 i i 段区间 I i I_i 是能够变为 T i T_i 的最短的区间,则 S S 能够变为 T T 当且仅当剩余部分 I T + 1 I_{|T|+1} 的总和为 0 0

这是因为若最后一段 I T I_{|T|} 连接上 I T + 1 I_{|T|+1} 后存在相邻的相同字符,则可以直接将 I T + 1 I_{|T|+1} 划入 I T I_{|T|} ,否则,它们拼接后一定形如 ababa 或 babab ,可以递归找到一组划分方式。

由此,可以通过 DP 对 T T 进行计数。

时间复杂度 O ( N ) O(N)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;
const int P = 1e9 + 7;
typedef long long ll;
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
char s[MAXN];
int dp[MAXN], pre[MAXN], lst[MAXN][3], trans[MAXN][3];
void update(int &x, int y) {
	x += y;
	if (x >= P) x -= P;
}
int main() {
	scanf("%s", s + 1);
	int n = strlen(s + 1);
	memset(lst[0], -1, sizeof(lst[0]));
	for (int i = 1; i <= n; i++) {
		if (s[i] == 'a') pre[i] = (pre[i - 1] + 1) % 3;
		else pre[i] = (pre[i - 1] + 2) % 3;
		memcpy(lst[i], lst[i - 1], sizeof(lst[i - 1]));
		lst[i][pre[i - 1]] = i - 1;
	}
	memset(trans[0], -1, sizeof(trans[0]));
	for (int i = 1; i <= n; i++) {
		if (i != 1 && s[i - 1] == s[i]) memcpy(trans[i], lst[i], sizeof(lst[i]));
		else memcpy(trans[i], trans[i - 1], sizeof(trans[i - 1]));
	}
	if (trans[n][0] == -1) {
		cout << 1 << endl;
		return 0;
	}
	dp[n] = 1;
	for (int i = n; i >= 1; i--) {
		update(dp[i - 1], dp[i]);
		if (s[i] == 'a') {
			if (trans[i][(pre[i] + 1) % 3] != -1) {
				update(dp[trans[i][(pre[i] + 1) % 3]], dp[i]);
			}
		} else {
			if (trans[i][(pre[i] + 2) % 3] != -1) {
				update(dp[trans[i][(pre[i] + 2) % 3]], dp[i]);
			}
		}
	}
	int ans = 0;
	for (int i = 0; i <= n - 1; i++)
		if (pre[i] == 0) ans = (ans + dp[i]) % P;
	cout << ans << endl;
	return 0;
}

Problem F. Grafting

首先考虑答案不为 N N 的情况,此时一定存在一个没有被删除过的点 p o s pos

p o s pos 为根,在两棵树内各做一次 DFS ,结构相同的部分显然应当保留,而剩余部分必须全部删去。

在 A 树中找到一个 B 树中对应地父亲已经存在的叶子,删除之,并连向其父亲,重复该过程,若能还原 B 树,则步数即为删去的叶子数,否则,即不能在不操作 p o s pos 的情况下还原 B 树。

考虑答案为 N N 的情况,则我们只需要枚举第一步在 A 树中操作的节点即可。

时间复杂度 O ( T N 3 ) O(TN^3) ,可优化至 O ( T N 2 ) O(TN^2)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 105;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
int n, fa[MAXN], fb[MAXN];
vector <int> a[MAXN], b[MAXN];
void dfsa(int pos, int f) {
	fa[pos] = f;
	for (auto x : a[pos])
		if (x != f) dfsa(x, pos);
}
void dfsb(int pos, int f) {
	fb[pos] = f;
	for (auto x : b[pos])
		if (x != f) dfsb(x, pos);
}
bool vis[MAXN], unlock[MAXN], used[MAXN];
void dfs(int pos) {
	vis[pos] = true;
	for (auto x : a[pos])
		if (fa[x] == pos && fb[x] == pos) dfs(x);
}
bool root(int pos) {
	memset(unlock, false, sizeof(unlock)), unlock[pos] = true;
	memset(used, false, sizeof(used));
	for (int t = 1; t <= n; t++) {
		int pos = 0;
		for (int i = 1; i <= n; i++)
			if (!used[i] && unlock[i]) {
				int d = 0;
				for (auto x : a[i])
					if (!used[x]) d++;
				if (d <= 1) pos = i;
			}
		if (pos == 0) return false;
		used[pos] = true;
		for (auto x : b[pos])
			unlock[x] = true;
	}
	return true;
}
int work(int pos) {
	memset(vis, false, sizeof(vis)), dfs(pos);
	memcpy(unlock, vis, sizeof(vis));
	for (int i = 1; i <= n; i++)
		if (vis[i]) {
			for (auto x : b[i])
				unlock[x] = true;
		}
	memset(used, false, sizeof(used));
	int cnt = 0;
	for (int i = 1; i <= n; i++)
		if (!vis[i]) cnt++;
	for (int t = 1; t <= cnt; t++) {
		int pos = 0;
		for (int i = 1; i <= n; i++)
			if (!vis[i] && !used[i] && unlock[i]) {
				int d = 0;
				for (auto x : a[i])
					if (!used[x]) d++;
				if (d <= 1) pos = i;
			}
		if (pos == 0) return n + 1;
		used[pos] = true;
		for (auto x : b[pos])
			unlock[x] = true;
	}
	return cnt;
}
void solve() {
	int ans = n + 1;
	for (int i = 1; i <= n; i++) {
		dfsa(i, 0);
		dfsb(i, 0);
		chkmin(ans, work(i));
		if (root(i)) chkmin(ans, n);
	}
	if (ans == n + 1) puts("-1");
	else cout << ans << endl;
}
int main() {
	int T; read(T);
	while (T--) {
		read(n);
		for (int i = 1; i <= n; i++) {
			a[i].clear();
			b[i].clear();
		}
		for (int i = 1; i <= n - 1; i++) {
			int x, y; read(x), read(y);
			a[x].push_back(y);
			a[y].push_back(x);
		}
		for (int i = 1; i <= n - 1; i++) {
			int x, y; read(x), read(y);
			b[x].push_back(y);
			b[y].push_back(x);
		}
		solve();
	}
	return 0;
}

Atcoder Grand Contest 28

Problem C. Min Cost Cycle

考虑选定 N N 个数值,判断是否能够连边使得所有边两侧的数值恰好有一个被选中,且所有点在一个环内。

可以发现条件是满足如下至少一点
( 1 ) (1) 、选择数值均为同侧数值
( 2 ) (2) 、存在一个位置两侧的数值均被选中

对于 ( 2 ) (2) ,枚举该位置,然后贪心选择最小的 N 2 N-2 个数值即可。

时间复杂度 O ( N L o g N ) O(NLogN)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
ll a[MAXN], b[MAXN], s[MAXN];
map <int, int> mp; int n, m;
pair <int, int> x[MAXN];
int main() {
	read(n), m = 0;
	ll sa = 0, sb = 0;
	for (int i = 1; i <= n; i++) {
		read(a[i]), read(b[i]);
		sa += a[i], sb += b[i];
		x[++m] = make_pair(a[i], i);
		x[++m] = make_pair(b[i], i);
	}
	sort(x + 1, x + m + 1);
	for (int i = 1; i <= m; i++) {
		mp[x[i].first] = i;
		s[i] = s[i - 1] + x[i].first;
	}
	ll ans = min(sa, sb);
	for (int i = 1; i <= n; i++) {
		int x = mp[a[i]], y = mp[b[i]];
		ll cur = a[i] + b[i]; int pos = n - 2;
		if (x > y) swap(x, y), swap(a[i], b[i]);
		if (x <= pos) {
			cur -= a[i];
			pos++;
		}
		if (y <= pos) {
			cur -= b[i];
			pos++;
		}
		chkmin(ans, cur + s[pos]);
	}
	cout << ans << endl;
	return 0;
}

Problem D. Chords

考虑如何计算一个连边方案的连通块个数。

一种可行的计算方案即为 i = 1 N j = i N E ( i , j ) \sum_{i=1}^{N}\sum_{j=i}^{N}E(i,j) ,其中 E ( i , j ) E(i,j) 表示是否存在一个最小值为 i i ,最大值为 j j 的连通块。

因此,记 d p i , j dp_{i,j} 表示存在最小值为 i i ,最大值为 j j 的连通块的划分方案数,答案即为对 dp 数组求和的结果。 d p i , j dp_{i,j} 可以通过枚举 i i 所在连通块的大小简单转移。

时间复杂度 O ( N 3 ) O(N^3)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 605;
const int P = 1e9 + 7;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
int s[MAXN], home[MAXN], dp[MAXN][MAXN];
int n, m, p[MAXN], fac[MAXN];
void update(int &x, int y) {
	x += y;
	if (x >= P) x -= P;
}
int main() {
	read(n), read(m), fac[0] = 1;
	for (int i = 1; i <= n; i++)
		fac[i] = fac[i - 1] * (2ll * i - 1) % P;
	for (int i = 1; i <= m; i++) {
		int x, y; read(x), read(y);
		home[x] = home[y] = i;
	}
	int ans = 0;
	for (int i = 1; i <= n * 2; i++) {
		static bool col[MAXN]; int cnt = 0, empty = 0;
		memset(col, false, sizeof(col));
		for (int j = i; j <= n * 2; j++) {
			if (home[j]) {
				cnt -= col[home[j]];
				col[home[j]] ^= true;
				cnt += col[home[j]];
			} else empty += 1;
			if (cnt == 0 && empty % 2 == 0) {
				dp[i][j] = fac[empty / 2];
				for (int k = i, cmt = 0; k <= j - 1; k++) {
					cmt += !home[k];
					if (cmt % 2 == 0) update(dp[i][j], P - 1ll * dp[i][k] * fac[empty / 2 - cmt / 2] % P);
				}
				update(ans, 1ll * dp[i][j] * fac[n - m - empty / 2] % P);
			}
		}
	}
	cout << ans << endl;
	return 0;
}

Problem E. High Elements

考虑逐位确定,形式化地,我们需要解决形如 “当前 X X 数组前缀最大值有 C n t x Cntx 个,最大值为 M a x Max Y Y 数组前缀最大值有 C n t y Cnty 个,最大值为 M a y May ;能否通过合理地选择 S S 的最后 x x 位使得最终的要求被满足” 。

假设最终 X X 数组的前缀最大值为 , M a x , a 1 , a 2 , , a a \dots,Max,a_1,a_2,\dots,a_{|a|} Y Y 数组的前缀最大值为 , M a y , b 1 , b 2 , , b b \dots,May,b_1,b_2,\dots,b_{|b|} ,那么, a , b a,b 应当满足如下条件:
( 1 ) (1) M a x < a 1 < a 2 < < a a Max<a_1<a_2<\dots<a_{|a|} M a y < b 1 < b 2 < < b b May<b_1<b_2<\dots<b_{|b|}
( 2 ) (2) C n t x + a = C n t y + b Cntx+|a|=Cnty+|b|
( 3 ) (3) 、原序列中的前缀最大值均出现在 a a b b 中。

并且,由于我们可以任意地删除不是原序列中的前缀最大值的元素,因此,我们不妨认为 a , b a,b 中至少有一者仅由原序列中的前缀最大值组成。

不妨先考虑 a a 仅由原序列中的前缀最大值组成的情况,那么,显然 a a 可以被 b b 唯一确定,则 b b 需要满足如下条件:
( 1 ) (1) M a y < b 1 < b 2 < < b b May<b_1<b_2<\dots<b_{|b|}
( 2 ) (2) C n t x + Q k = C n t y + b Cntx+Q−k=Cnty+|b| ,其中 Q Q 表示原序列中的前缀最大值的剩余个数, k k 表示用在 b b 中的原序列前缀最大值的个数

m = Q k m=Q-k ,即不属于原序列前缀最大值的位置个数,则 ( 2 ) (2) 可以写为 2 k + m = C n t x C n t y + Q 2k+m=Cntx−Cnty+Q

那么,问题变转化为了某些数值有权值 1 1 ,剩余数值有权值 2 2 ,选择一个上升序列,使得选定的数值的权值和为 C n t x C n t y + Q Cntx−Cnty+Q

注意到如果存在选择权值和为 c + 2 c+2 的方案,就会存在选择权值和为 c c 的方案,因此,我们只需要对每一种奇偶性,计算可以选取的最大权值和即可。

可以用线段树进行上式动态规划。

时间复杂度 O ( N L o g N ) O(NLogN)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
struct SegmentTree {
	struct Node {
		int lc, rc;
		int tag, Max;
	} a[MAXN * 2];
	int n, root, size;
	void build(int &root, int l, int r) {
		root = ++size;
		a[root].tag = 0;
		a[root].Max = 0;
		if (l == r) return;
		int mid = (l + r) / 2;
		build(a[root].lc, l, mid);
		build(a[root].rc, mid + 1, r);
	}
	void init(int x) {
		n = x, root = size = 0;
		build(root, 1, n);
	}
	void update(int root) {
		a[root].Max = max(a[a[root].lc].Max, a[a[root].rc].Max);
	}
	void pushdown(int root) {
		if (a[root].tag) {
			a[a[root].lc].tag += a[root].tag;
			a[a[root].lc].Max += a[root].tag;
			a[a[root].rc].tag += a[root].tag;
			a[a[root].rc].Max += a[root].tag;
			a[root].tag = 0;
		}
	}
	void modify(int root, int l, int r, int ql, int qr, int d) {
		if (l == ql && r == qr) {
			a[root].tag += d;
			a[root].Max += d;
			return;
		}
		int mid = (l + r) / 2;
		pushdown(root);
		if (mid >= ql) modify(a[root].lc, l, mid, ql, min(mid, qr), d);
		if (mid + 1 <= qr) modify(a[root].rc, mid + 1, r, max(mid + 1, ql), qr, d);
		update(root);
	}
	void modify(int l, int r, int d) {
		modify(root, 1, n, l, r, d);
	}
	int query(int root, int l, int r, int ql, int qr) {
		if (l == ql && r == qr) return a[root].Max;
		int mid = (l + r) / 2; pushdown(root);
		if (mid >= qr) return query(a[root].lc, l, mid, ql, qr);
		else if (mid + 1 <= ql) return query(a[root].rc, mid + 1, r, ql, qr);
		else return max(query(a[root].lc, l, mid, ql, mid), query(a[root].rc, mid + 1, r, mid + 1, qr));
	}
	int query(int l, int r) {
		if (l > r) return 0;
		else return query(root, 1, n, l, r);
	}
} ST[2];
int n, a[MAXN], value[MAXN], dp[MAXN][2], suf[MAXN];
bool check(int pos, int cntx, int cnty, int Max, int May) {
	int x = cntx - cnty + suf[pos + 1];
	if (x >= 0 && ST[x & 1].query(May + 1, n) >= x) return true;
	x = cnty - cntx + suf[pos + 1];
	if (x >= 0 && ST[x & 1].query(Max + 1, n) >= x) return true;
	return false;
}
int main() {
	read(n);
	for (int i = 1, Max = 0; i <= n; i++) {
		read(a[i]), chkmax(Max, a[i]);
		if (Max == a[i]) value[i] = 2;
		else value[i] = 1;
	}
	for (int i = n; i >= 1; i--)
		suf[i] = suf[i + 1] + value[i] - 1;
	ST[0].init(n), ST[1].init(n);
	for (int i = n; i >= 1; i--) {
		for (int j = 0; j <= 1; j++) {
			int cur = value[i] + ST[j].query(a[i], n);
			chkmax(dp[i][cur & 1], cur);
		}
		ST[0].modify(a[i], a[i], dp[i][0]);
		ST[1].modify(a[i], a[i], dp[i][1]);
	}
	string ans;
	int cntx = 0, cnty = 0, Max = 0, May = 0;
	if (!check(0, cntx, cnty, Max, May)) {
		puts("-1");
		return 0;
	}
	for (int i = 1; i <= n; i++) {
		ST[0].modify(a[i], a[i], -dp[i][0]);
		ST[1].modify(a[i], a[i], -dp[i][1]);
		if (check(i, cntx + (a[i] >= Max), cnty, max(a[i], Max), May)) {
			cntx += a[i] >= Max;
			chkmax(Max, a[i]);
			ans += '0';
		} else {
			cnty += a[i] >= May;
			chkmax(May, a[i]);
			ans += '1';
		}
	}
	cout << ans << endl;
	return 0;
}

Problem F. Reachable Cells

考虑对于每一个点 ( i , j ) (i,j) ,计算 r e s i , j res_{i,j} 表示从该点可以到达的位置的权值和。

为了计算 r e s i , j res_{i,j} ,我们还需要辅助数组 l i , j , k , r i , j , k l_{i,j,k},r_{i,j,k} 表示 ( i , j ) (i,j) 可以到达的第 k k 行的最左、最右侧的位置,若不存在,则分别记为 + , +\infty,-\infty ,辅助数组的转移较为显然。

关于辅助数组的意义,考虑从下到上,从右到左处理各个位置,若一个位置已经不存在尚未处理过的位置能够到达,则将其标为障碍不会对问题产生影响。并且,若如此做,一个正在处理的位置的 l i , j , k , r i , j , k l_{i,j,k},r_{i,j,k} 就不重不漏的描述了 ( i , j ) (i,j) 在第 k k 行能够到达的位置,因此实际上维护前缀和即可得出一个 O ( N 3 ) O(N^3) 的做法,下文的做法经过了更多思考,常数更小。

考虑如何描述 ( i + 1 , j ) (i+1,j) ( i , j + 1 ) (i,j+1) 都能到达的位置,可以发现,这些位置能够用一组两维坐标均递增的点 ( x k , y k ) (x_k,y_k) 能够到达的位置来描述。其中 ( x 1 , y 1 ) (x_1,y_1) ( i + 1 , j ) (i+1,j) ( i , j + 1 ) (i,j+1) 都能到达的两维坐标最小的点,此处由于问题的性质, “两维坐标最小” 的说法是不存在歧义的; ( x 2 , y 2 ) (x_2,y_2) ( x 1 , y 1 ) (x_1,y_1) 无法达到,且 ( i + 1 , j ) (i+1,j) ( i , j + 1 ) (i,j+1) 都能到达的两维坐标最小的点,以此类推。

那么,
r e s i , j = a i , j + r e s i + 1 , j + r e s i , j + 1 k r e s x k , y k res_{i,j}=a_{i,j}+res_{i+1,j}+res_{i,j+1}-\sum_{k}res_{x_k,y_k}

关于 ( x k , y k ) (x_k,y_k) 的计算可以参考下文代码。

时间复杂度 O ( N 3 ) O(N^3) ,常数较小,可以通过 N = 1500 N=1500 的数据范围。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1505;
typedef long long ll;
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
char s[MAXN][MAXN];
short l[MAXN][MAXN], r[MAXN][MAXN];
int n, a[MAXN][MAXN], res[MAXN][MAXN];
inline void chkmax(short &x, short y) {if (x < y) x = y; }
inline void chkmin(short &x, short y) {if (x > y) x = y; } 
int main() {
	read(n);
	for (int i = 1; i <= n; i++) {
		scanf("%s", s[i] + 1);
		for (int j = 1; j <= n; j++)
			if (s[i][j] == '#') a[i][j] = 0;
			else a[i][j] = s[i][j] - '0';
	}
	ll finalans = 0;
	for (int i = n; i >= 1; i--)
	for (int j = n; j >= 1; j--) {
		if (a[i][j] != 0) {
			if (a[i + 1][j] == 0 && a[i][j + 1] == 0) l[j][i] = r[j][i] = j;
			else if (a[i + 1][j] == 0) {
				memcpy(l[j] + i, l[j + 1] + i, sizeof(short) * (n - i + 1));
				memcpy(r[j] + i, r[j + 1] + i, sizeof(short) * (n - i + 1));
				l[j][i] = j, res[i][j] = res[i][j + 1];
			} else if (a[i][j + 1] == 0) {
				l[j][i] = r[j][i] = j;
				res[i][j] = res[i + 1][j];
			} else {
				short Max = 0; int ans = res[i + 1][j] + res[i][j + 1];
				l[j][i] = j, r[j][i] = r[j + 1][i];
				for (int k = i + 1; k <= n; k++) {
					if (r[j][k] >= l[j + 1][k] && l[j + 1][k] > Max) ans -= res[k][l[j + 1][k]];
					chkmax(Max, r[j][k]);
					chkmax(r[j][k], r[j + 1][k]);
					chkmin(l[j][k], l[j + 1][k]);
				}
				res[i][j] = ans;
			}
			finalans += a[i][j] * res[i][j];
			res[i][j] += a[i][j];
		} else {
			memset(l[j] + i, 0x3f, sizeof(short) * (n - i + 1));
			memset(r[j] + i, 0, sizeof(short) * (n - i + 1));
		}
	}
	cout << finalans << endl;
	return 0;
}

Atcoder Grand Contest 29

Problem C. Lexicographic constraints

首先二分答案 p p ,此后,一个字符串可以看做一个 1 0 9 10^9 位的 p p 进制数,并且只有前 A i A_i 位可以非零。

不难得到一个贪心解法,即每次贪心地选取恰好大于上一个数的一个数。

由于 A i A_i 范围较大,不方便直接维护高精度数,可以考虑维护高精度数中非零的数位。

时间复杂度 O ( N L o g N ) O(NLogN)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;
typedef long long ll;
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
int n, top, a[MAXN];
pair <int, int> s[MAXN];
bool check(int p) {
	if (p == 1) {
		bool ans = true;
		for (int i = 1; i <= n; i++)
			ans &= a[i] > a[i - 1];
		return ans;
	}
	top = 0;
	for (int i = 1; i <= n; i++) {
		if (a[i] <= a[i - 1]) {
			while (top >= 1 && s[top].first > a[i]) top--;
			int pos = a[i];
			while (top >= 1) {
				if (s[top].first == pos) {
					s[top].second++;
					if (s[top].second == p) {
						top--;
						pos--;
					} else {
						pos = -1;
						break;
					}
				} else {
					s[++top] = make_pair(pos, 1);
					pos = -1; break;
				}
			}
			if (pos == 0) return false;
			else if (pos != -1) s[++top] = make_pair(pos, 1);
		}
	}
	return true;
}
int main() {
	read(n); int l = 1, r = n;
	for (int i = 1; i <= n; i++)
		read(a[i]);
	while (l < r) {
		int mid = (l + r) / 2;
		if (check(mid)) r = mid;
		else l = mid + 1;
	}
	cout << l << endl;
	return 0;
}

Problem E. Wandering TKHS

考虑直接与 1 1 号点相连的点,它们的答案显然为 1 1 ,且这些子树均相互独立。

考虑一个以 x x 号节点为根的子树,子树内的点只需要达到 x x ,再走一步即可达到 1 1 。因此, x x 的各个子树也是相互独立的。考虑一个以 y y 为根的子树,从 y y 出发,首先会访问所有附近的 < x < x 的数组成的连通块,再访问 x x ,因此,这些点的答案是相同的。由于没有被访问到的子树均以一个 > x >x 的数为根,此时各个子树的答案依然是独立的。

考虑一个以 z   ( z > x ) z\ (z>x) 号点为根的子树,首先,仅通过 < x <x 的点到达 z z 的点在从 z z 的子树中出发的情况均会被访问,剩余的各个子树是独立的,可以用类似的方式递归处理。

时间复杂度 O ( N L o g N ) O(NLogN)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
vector <int> a[MAXN];
int n, f[MAXN], p[MAXN], s[MAXN], ans[MAXN];
void dfs(int pos, int fa) {
	f[pos] = fa;
	for (auto x : a[pos])
		if (x != fa) dfs(x, pos);
}
int find(int x) {
	if (p[x] == x) return x;
	else return p[x] = find(p[x]);
}
void work(int pos, int fa, int sum) {
	vector <int> mem;
	priority_queue <int, vector <int>, greater <int>> Heap;
	while (!Heap.empty()) Heap.pop(); Heap.push(pos);
	while (!Heap.empty() && (Heap.top() == pos || Heap.top() <= fa)) {
		int tmp = Heap.top(); Heap.pop(), sum++;
		for (auto x : a[tmp]) if (f[x] == tmp) Heap.push(x), mem.push_back(x), p[x] = (tmp == pos) ? x : tmp;
	}
	ans[pos] = sum;
	while (!Heap.empty() && (Heap.top() == pos || Heap.top() <= pos)) {
		int tmp = Heap.top(); Heap.pop(), s[find(tmp)]++;
		for (auto x : a[tmp]) if (f[x] == tmp) Heap.push(x), mem.push_back(x), p[x] = tmp;
	}
	for (auto x : mem) ans[x] = sum + s[find(x)];
	while (!Heap.empty()) {
		work(Heap.top(), pos, sum + s[find(Heap.top())]);
		Heap.pop();
	}
}
int main() {
	read(n);
	for (int i = 1; i <= n - 1; i++) {
		int x, y; read(x), read(y);
		a[x].push_back(y);
		a[y].push_back(x);
	}
	dfs(1, 0);
	for (auto x : a[1])
		work(x, 1, 0);
	for (int i = 2; i <= n; i++)
		printf("%d ", ans[i]);
	return 0;
}

Problem F. Construction of a tree

考虑有解的必要条件,首先,必须存在一个点集到点集中任意一点的完美匹配。

其次, { 1 , 2 , , N } \{1,2,\dots,N\} 不能被划分为非空的两个子集 S , T S,T ,使得不存在横跨 S , T S,T E i E_i

可以发现,对于满足以上两点的输入,只要先求得一个完美匹配,再从唯一的没有被匹配的点出发进行 BFS 即可构造出一组解。

用 Dinic 算法实现求解最大匹配,时间复杂度 O ( E i N ) O(\sum|E_i|\sqrt{N})

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
namespace NetworkFlow {
	const int INF = 2e9;
	const int MAXP = 2e5 + 5;
	struct edge {
		int dest, flow;
		unsigned pos;
	};
	vector <edge> a[MAXP];
	int tot, s, t, dist[MAXP];
	unsigned curr[MAXP];
	void addedge(int x, int y, int z) {
		a[x].push_back((edge) {y, z, a[y].size()});
		a[y].push_back((edge) {x, 0, a[x].size() - 1});
	}
	int dinic(int pos, int limit) {
		if (pos == t) return limit;
		int used = 0, tmp;
		for (unsigned &i = curr[pos]; i < a[pos].size(); i++)
			if (a[pos][i].flow != 0 && dist[pos] + 1 == dist[a[pos][i].dest] && (tmp = dinic(a[pos][i].dest, min(limit - used, a[pos][i].flow)))) {
				used += tmp;
				a[pos][i].flow -= tmp;
				a[a[pos][i].dest][a[pos][i].pos].flow += tmp;
				if (used == limit) return used;
			}
		return used;
	}
	bool bfs() {
		static int q[MAXP];
		int l = 0, r = 0;
		memset(dist, 0, sizeof(dist));
		dist[s] = 1, q[0] = s;
		while (l <= r) {
			int tmp = q[l];
			for (unsigned i = 0; i < a[tmp].size(); i++)
				if (dist[a[tmp][i].dest] == 0 && a[tmp][i].flow != 0) {
					q[++r] = a[tmp][i].dest;
					dist[q[r]] = dist[tmp] + 1;
				}
			l++;
		}
		return dist[t] != 0;
	}
	int flow() {
		int ans = 0;
		while (bfs()) {
			memset(curr, 0, sizeof(curr));
			ans += dinic(s, INF);
		}
		return ans;
	}
	void getmatch(int n, int *match) {
		for (int i = 2; i <= n; i++) {
			for (auto x : a[i])
				if (x.dest > n && x.flow == 0) {
					match[i] = x.dest - n;
					break;
				}
		}
	}
}
bool vis[MAXN], used[MAXN];
int n, match[MAXN];
pair <int, int> ans[MAXN];
vector <int> a[MAXN], b[MAXN];
int main() {
	read(n);
	NetworkFlow :: s = 1;
	NetworkFlow :: t = n * 2 + 1;
	for (int i = 2; i <= n; i++) {
		int k; read(k);
		NetworkFlow :: addedge(1, i, 1);
		while (k--) {
			int x; read(x);
			a[i].push_back(x);
			b[x].push_back(i);
			NetworkFlow :: addedge(i, x + n, 1);
		}
	}
	for (int i = 1; i <= n; i++)
		NetworkFlow :: addedge(i + n, n * 2 + 1, 1);
	if (NetworkFlow :: flow() < n - 1) {
		puts("-1");
		return 0;
	}
	NetworkFlow :: getmatch(n, match);
	for (int i = 1; i <= n; i++)
		vis[i] = true;
	for (int i = 2; i <= n; i++)
		vis[match[i]] = false;
	queue <int> q;
	for (int i = 1; i <= n; i++)
		if (vis[i]) {
			for (auto x : b[i]) {
				q.push(x);
				used[x] = true;
			}
		}
	while (!q.empty()) {
		int tmp = q.front();
		for (auto x : a[tmp])
			if (vis[x]) {
				ans[tmp] = make_pair(x, match[tmp]);
				break;
			}
		vis[match[tmp]] = true;
		for (auto x : b[match[tmp]])
			if (!used[x]) {
				q.push(x);
				used[x] = true;
			}
		q.pop();
	}
	for (int i = 2; i <= n; i++)
		if (ans[i].first == 0) {
			puts("-1");
			return 0;
		}
	for (int i = 2; i <= n; i++)
		printf("%d %d\n", ans[i].first, ans[i].second);
	return 0;
}
发布了813 篇原创文章 · 获赞 93 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/qq_39972971/article/details/103709069