Codeforces Round #740 (Div. 2)

Codeforces Round #740 (Div. 2)

上 大 分 😄

tourist 场就是不一样,感觉质量很高而且思维难度强。

\(\mathcal A\)

求一个序列奇偶排序的轮次。

复杂度显然可以证明是 \(O(n^2)\) 的,所以按题意模拟就好。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 1010;
int n, a[N];

int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

bool Chck() {
	for(int i = 1; i < n; i ++)
		if(a[i] > a[i + 1]) return false;
	return true;
}

int main() {
	int T = read();
	while(T --) {
		n = read();
		for(int i = 1; i <= n; i ++) a[i] = read();
		if(Chck()) {puts("0"); continue;}
		for(int i = 1; ; i ++) {
			if(i & 1) {
				for(int j = 1; j <= n - 2; j += 2)
					if(a[j] > a[j + 1]) swap(a[j], a[j + 1]);
			}
			else {
				for(int j = 2; j <= n - 1; j += 2)
					if(a[j] > a[j + 1]) swap(a[j], a[j + 1]);
			}
			if(Chck()) {printf("%d\n", i); break;}
		}
	}
	return 0;
}

\(\mathcal B\)

有一个游戏,由 \(A,B\) 两人轮流进行自己的回合,每回合赢的人得一分。

在自己回合赢的叫做 holds server,而在对方回合胜利则称为 breaks serve

现在给出 \(A,B\) 的分别最终得分,不知道谁先开始,也不知道具体情况,求可能的 breaks 个数。

不失一般性,设 \(A>B,n=A+B,t=\lfloor\frac{n+1}{2}\rfloor\)

最小的情况,一定是 \(A\) 先并且自己回合全都 holds\(B\) 得分的自己回合全都 holds,其它给 \(A\) breaks,即:

\[\min_{\text{breaks}}=A-t \]

而且最小的情况可以通过 减少一个 \(A\)server,增多一个 \(B\)breaks 来不断 \(+2\)

最大的情况,一定是 \(B\) 先,并且自己的回合全都被 \(A\) breaks

然后 \(B\) 得分的回合也全都是 breaks \(A\) 的,其它给 \(A\) holds,即:

\[\max_{\text{breaks}}=t+\lfloor\frac{n}{2}\rfloor-(A-t) \]

相似的,最大的情况也可以通过操作不断 \(-2\)

那么答案就是在 \([\min,\max]\) 中与 \(\min\)\(\max\) 奇偶性相同的值。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 3e5 + 10;
bool ans[N];

int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

int main() {
	int T = read();
	while(T --) {
		int a = read(), b = read(), n = a + b;
		if(a < b) swap(a, b);
		int L = a - (n + 1) / 2;
		int R = (n + 1) / 2 + n / 2 - (a - (n + 1) / 2);
		for(int i = L; i <= R; i += 2) ans[i] = true;
		for(int i = R; i >= L; i -= 2) ans[i] = true;
		int sum = 0;
		for(int i = L; i <= R; i ++) sum += ans[i];
		printf("%d\n", sum);
		for(int i = L; i <= R; i ++) if(ans[i]) printf("%d ", i);
		puts("");
		for(int i = L; i <= R; i ++) ans[i] = false;
	}
	return 0;
}

\(\mathcal C\)

有一个人前去杀龙,有 \(n\) 个洞穴,第 \(i\) 个洞穴有 \(k_i\) 条龙,其中的第 \(j\) 条龙有战斗力 \(a_{i,j}\)

进了一个洞穴就必须杀完其中所有的龙,且必须按编号顺序从小到大杀。

这个人能杀死一条龙,当且仅当它的战斗力严格大于这条龙,杀了之后这个人的战斗力 \(+1\)

进入洞穴的顺序是任意的,求能够杀死所有龙的最小初始战斗力。

好像是我想复杂了的样子 QwQ。

可以预处理出每个洞穴进之前需要能出的来的最小战斗力 \(A_i\)

\[A_i=\max\limits_{1\leq j\leq k_i}\{a_{i,j}-(j-2)\} \]

\(A_i\) 排个序,没了。

比赛的时候认为不好证上面这个贪心的正确性\fad,然后就写了另一个同样正确的贪心。

方法还是纪念一下叭,毕竟感觉挺厉害的样子,是刚学的套路呢。

倒序思考,设 \(x_i\) 表示打第 \(i\sim n\) 个洞穴需要的代价,假设已经确定了贪心顺序,那么:

\[x_i=\max(x_{i+1}-k_i,A_i) \]

然后考虑微扰法,讨论 \(i,j\) 的顺序(这里指的“先”是离 \(n\) 近,因为倒序是考虑),将 \(\max\) 合并:

  1. \(i\) 先,\(Ans=\max(x-k_i-k_j,A_i-k_j,A_j)\)
  2. \(j\) 先,\(Ans=\max(x-k_i-k_j,A_j-k_i,A_i)\)

那么 \(i<j\) 更优当且仅当,\(\max(A_i-k_j,A_j)<\max(A_j-k_i,A_i)\),这就是大部分的贪心思路。

但是若两者相等,为了确保贪心顺序是严格全序关系,再加入 \(A_i>A_j\) 作为次关键字即可。

然后还可以拓展到求所有前缀,加入一棵线段树维护区间合并即可/qq

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long LL;
const int N = 1e5 + 10;
int n, k[N], a[N], c[N], p[N];

int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

bool cmp(int i, int j) {
	int X = max(a[i] - k[j], a[j]);
	int Y = max(a[j] - k[i], a[i]);
	if(X != Y) return X < Y;
	return a[i] > a[j];
}

int main() {
	int T = read();
	while(T --) {
		n = read();
		for(int i = 1; i <= n; i ++) {
			k[i] = read(), a[i] = 0, p[i] = i;
			for(int j = 1; j <= k[i]; j ++) c[j] = read();
			for(int j = 1; j <= k[i]; j ++) a[i] = max(a[i], c[j] - (j - 2));
		}
		sort(p + 1, p + n + 1, cmp);
		LL ans = 0, sum = 0;
		for(int i = n; i >= 1; i --)
			ans = max(ans, a[p[i]] - sum), sum += k[p[i]];
		printf("%lld\n", ans);
	}
	return 0;
}

\(\mathcal{D1\&D2}\)

给定数字 \(n\),每次可以将 \(n\) 减去 \(1\sim n-1\) 中的某个数,或除以 \(2\sim n\) 中的某个数。

求到达 \(1\) 的方案数。

方法一,简单 DP,相当于:

\[f(i)=\sum\limits_{j=1}^{i-1} f(j)+\sum\limits_{j=2}^i f(\lfloor\frac{i}{j}\rfloor) \]

得到 \(f(n)\) 即为答案,时间复杂度 \(O(n^2)\),不太行。

方法二,记录后缀 \(f\),然后整除分块找能到达的点,时间复杂度 \(O(n\sqrt n)\),可以通过 D1:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long LL;
const int N = 2e5 + 10;
LL n, P, f[N];

LL read() {
	LL x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

int main() {
	n = read(), P = read();
	f[n] = 1; LL sum = 0;
	for(int i = n; i > 1; i --) {
		f[i] = (f[i] + sum) % P;
		for(LL l = 2, r; l <= i; l = r + 1) {
			r = (i / (i / l));
			f[i / l] = (f[i / l] + f[i] * (r - l + 1) % P) % P;
		}
		sum = (sum + f[i]) % P;
	}
	printf("%lld\n", (f[1] + sum) % P);
	return 0;
}

方法三,同样还是维护后缀和,但是每个点枚举除数 \(j\),即看哪些数字 \(x\) 满足 \(\lfloor\frac{x}{j}\rfloor =i\)

显然,\(j\in[i\times j,(i+1)\times j-1]\),时间复杂度是调和级数 \(O(n\log n)\) 的。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long LL;
const int N = 4e6 + 10;
int n; LL P, f[N], g[N];

int main() {
	scanf("%d %lld", &n, &P);
	f[n] = g[n] = 1;
	for(int i = n - 1; i >= 1; i --) {
		f[i] = g[i + 1];
		for(int j = 2; i * j <= n; j ++) {
			int L = i * j;
			int R = min(n, (i + 1) * j - 1);
			if(L <= R) f[i] = (f[i] + g[L] + P - g[R + 1]) % P; 
		}
		g[i] = (g[i + 1] + f[i]) % P;
	}
	printf("%lld\n", f[1]);
	return 0;
} 

\(\mathcal E\)

给定一个 \(n\) 的排列,每次可以将前 \(1\sim p\) 位置上的数翻转,求在 \(\frac{5n}{2}\) 步内将其排序(升序),或输出无解。

保证 \(n\) 为奇数,必须保证 \(p\) 为奇数。

这个 mathcal 之后的 E 怎么这么鬼畜/fad

这个奇数看着就很故意,考虑翻转操作的本质,即将 \(i\)\(p-i+1\) 互换,因为 \(p\) 为奇数,所以奇偶性不变。

即问题有解,当且仅当初始的 \(i\equiv a_i\pmod 2\),下面的构造保证了在规定步数内有解。

因为是前缀操作,所以若 \(n-1,n\) 都到了位,那么可以令 \(n-2\) 来缩小问题规模,同时保证 \(n\) 为奇数。

递归会进行 \(\frac{n-1}{2}\) 轮,若每次缩小规模只使用 \(\leq 5\) 次即可得到解,一种方法如下:

  1. 设当前 \(n\) 在位置 \(x\),翻转前缀 \(x\),将 \(n\) 变到首位。(注意因为有解,所以 \(x\) 一定为奇数)
  2. 设当前 \(n-1\) 在位置 \(y\),翻转前缀 \(y-1\),将 \(n\) 变到 \(n-1\) 前一位。(同理,\(y\) 一定为偶数)
  3. 翻转前缀 \(y+1\),将 \(n-1,n\) 分别移动到 \(2,3\) 位。
  4. 翻转前缀 \(3\),将 \(n,n-1\) 分别移动到 \(1,2\) 位。
  5. 翻转前缀 \(n\),达到目标。

于是用 \(\frac{5(n-1)}{2}\) 步得到了答案。

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;

const int N = 2030;
int n, a[N], b[N];
vector<int> ans;

int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

int Get(int p, int n) {
	for(int i = 1; i <= n; i ++)
		if(a[i] == p) return i;
}

void Reverse(int p) {
	ans.push_back(p);
	for(int i = 1; i <= p; i ++) b[i] = a[i];
	for(int i = 1; i <= p; i ++) a[i] = b[p - i + 1];
} 

void Work(int n) {
	if(n == 1) return;
	int x = Get(n, n);     Reverse(x);
	int y = Get(n - 1, n); Reverse(y - 1);
	Reverse(y + 1); Reverse(3);
	Reverse(n); Work(n - 2);
}

int main() {
	int T = read();
	while(T --) {
		n = read();
		for(int i = 1; i <= n; i ++) a[i] = read();

		bool flag = false;
		for(int i = 1; i <= n; i ++)
			if((i & 1) != (a[i] & 1)) {flag = true; break;}
		if(flag) {puts("-1"); continue;}

		ans.clear(); Work(n);
		printf("%d\n", ans.size());
		for(auto i : ans) printf("%d ", i);
		puts("");
	}
}

\(\mathcal F\)

给定插入排序的插入方案,求初始序列的方案数,每个数在 \([1,n]\) 范围内。

假设初始序列为 \([a_1,a_2,\cdots,a_n]\),那么最终序列根据插排的方案,是唯一确定的。

且一种合法的最终序列和初始序列构成双射,所以不妨考虑最终序列的方案数。

最终序列的某些位置是 \(a_i\leq a_{i+1}\),有些则是 \(a_i<a_{i+1}\),假设 \(<\) 的个数是 \(c\),那么:

\[Ans=\binom{2n-c-1}{n} \]

证明,得到一个最终序列 \(B\),扫描所有 \(a_i,a_{i+1}\) 之间的 \(n-1\) 个限制,若遇到 \(\leq\) 则将 \(b_{i+1}\sim b_n\)\(+1\)

那么现在的所有 \(i\) 都有 \(b_i<b_{i+1}\),构造出了一个与原序列的双射。

且数字的上限是 \(n+(n-1)-c\),那么显然总个数是 \(\binom{2n-c-1}{n}\)

考虑 \(c\) 的个数的求解,可能有人认为就是 \(m\),但是可惜是错的。

例如插入操作为 \((4,3),(5,4)\),那么先得到 \(a_1\leq a_2\leq a_4<a_3\),然后得到 \(a_1\leq a_2\leq a_4\leq a_5<a_3\)

总的 \(<\) 个数显然为 \(1\),那么我们需要一个支持动态插入 + 查询第 \(k\) 大的数据结构模拟操作来统计。

显然 Splay 就可以,但是这里需要复杂度为 \(O(m\log n)\) 而不是 \(O(n\log n)\),所以还必须维护加标记。

从而模拟整个序列被插入的情况,一定注意 Splay 的 Push_Down 一定要在结构被破坏之前进行。

这个和稳定的线段树的 Push_Down 可以任意进行不同。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long LL;
const int N = 4e5 + 10;
const int M = 2e5 + 10;
const LL P = 998244353;
int n, m, tot, root;
LL fac[N], inv[N];
struct Splay{int dat, add, fa, c[2];} t[N];

int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

LL Pow(LL a, LL b){
	LL sum = 1;
	for(; b; b >>= 1){
		if(b & 1) sum = sum * a % P;
		a = a * a % P;
	}
	return sum;
}

void Get_Inv(){
	int t = N - 10; fac[0] = 1;
	for(int i = 1; i <= t; i ++) fac[i] = fac[i - 1] * i % P;
	inv[t] = Pow(fac[t], P - 2);
	for(int i = t - 1; i >= 1; i --) inv[i] = inv[i + 1] * (i + 1) % P;
}

LL C(int n, int m){
	if(n < m) return 0;
	if(m == 0 || n == m) return 1; 
	return fac[n] * inv[m] % P * inv[n - m] % P;
}

void Clear() {root = tot = 0;}
void Con(int x, int y, int o) {t[t[x].fa = y].c[o] = x;}
int New() {tot ++, t[tot] = (Splay){0, 0, 0, 0, 0}; return tot;}
void Add(int x, int v) {if(x) t[x].dat += v, t[x].add += v;}
void Push_Down(int x) {int v; if((v = t[x].add)) Add(t[x].c[0], v), Add(t[x].c[1], v), t[x].add = 0;}

void Rotate(int x) {
	int Y = t[x].fa, Yson = (t[Y].c[1] == x);
	int R = t[Y].fa, Rson = (t[R].c[1] == Y);
	int B = t[x].c[Yson ^ 1];
	if(Y == root) root = x;
	Con(x, R, Rson), Con(Y, x, Yson ^ 1), Con(B, Y, Yson);
}

void Splay(int x) {
	while(t[x].fa) {
		int y = t[x].fa, z = t[y].fa;
		if(!z)
			Rotate(x);
		else if((x == t[y].c[1]) == (y == t[z].c[1]))
			Rotate(y), Rotate(x);
		else
			Rotate(x), Rotate(x);
	}
}

int Insert(int &x, int fa, int v, int o) {
	if(!x) {
		t[x = New()].dat = v + 1;
		t[x].fa = fa; return x;
	}
	Push_Down(x);// 这个 Push_Down 不能放在下面的位置。
	if(t[x].dat == v) {t[x].dat ++; return x;}
    //不能放在这里,因为可能上面的 if retuen 了,那么之后 Splay 就破坏的 x 的子树结构。
    //但是 x 的 add 标记还没有下传,之后的左右儿子可能已经有大变化了。
	if(t[x].dat >  v) return Insert(t[x].c[0], x, v, 0);
	else return Insert(t[x].c[1], x, v, 1);
}

int main() {
	int T = read(); Get_Inv();
	while(T --) {
		n = read(), m = read();
		Clear();
		for(int i = 1; i <= m; i ++) {
			int x = read(), y = read();
			int u = Insert(root, 0, y, 0);
			Splay(u); Add(t[u].c[1], 1);
		}
		printf("%lld\n", C(2 * n - tot - 1, n));
	}
	return 0;
}
posted @ 2021-08-26 22:16  LPF'sBlog  阅读(45)  评论(0编辑  收藏  举报