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

本文详细解析了IOI集训队的作业,涉及Atcoder Grand Contest系列的多道算法竞赛题目,包括Min Max Repetition、Encoding Subsets、Arcs on a Circle等,涵盖了动态规划、概率计算、树形结构等多种算法和问题解决策略。通过深入探讨每道题目的解题思路和复杂度分析,帮助读者提升算法设计和编程能力。
摘要由CSDN通过智能技术生成

Atcoder Grand Contest 20

Problem D. Min Max Repetition

不妨设 A ≤ B A\leq B AB ,对于 A > B A>B A>B 的情况,可以将序列倒置,并将所有的 A 和 B 取反,然后当做 A ≤ B A\leq B AB 的情况处理。

由题设,我们要求最长的相同字符长度不超过 L = ⌈ A B + 1 ⌉ L=\lceil\frac{A}{B+1}\rceil L=B+1A

由于我们要求字典序最小,所以,若暴力构造序列,所使用的算法应当如下:

任意时刻首先考虑在当前字符串后加 A ,若加上 A 后无法继续形成一个合法的串,则改为在当前字符串后加 B 。

由该构造算法,我们可以发现,所构造的串一定以若干个 A A … A B AA\dots AB AAAB 开头,每个循环节中均有 L L L A A A ,不妨二分计算最多可以以多少个 A A … A B AA\dots AB AAAB 开头。

再将若干个 A B B … B ABB\dots B ABBB 放在结尾,构造一个合法串即可。

单组询问的时间复杂度为 O ( L o g ( A + B ) + ( D − C ) ) O(Log(A+B)+(D-C)) O(Log(A+B)+(DC))

#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("");
}
char s[MAXN];
int n, m, l, r, d, len;
int main() {
	int T; read(T);
	while (T--) {
		read(n), read(m); bool flg = n < m;
		read(l), read(r), len = r - l + 1;
		if (flg) {
			swap(n, m);
			l = n + m - l + 1;
			r = n + m - r + 1;
			swap(l, r);
		}
		d = n / (m + 1) + (n % (m + 1) != 0); int j = 0;
		if (n >= m * d) {
			for (int i = l; i <= r; i++)
				if (i % (d + 1) == 0) s[++j] = 'B';
				else s[++j] = 'A';
		} else {
			int ql = 0, qr = n / d - 1;
			while (ql < qr) {
				int mid = (ql + qr + 1) / 2;
		 		int lfta = n - (mid + 1) * d;
				int lftb = m - mid;
				if (1ll * (lfta + 1) * d >= lftb) ql = mid;
				else qr = mid - 1;
			}
			for (int i = l; i <= min(ql * (d + 1) + d, r); i++)
				if (i % (d + 1) == 0) s[++j] = 'B';
				else s[++j] = 'A';
			l -= ql * (d + 1) + d; chkmax(l, 1);
			r -= ql * (d + 1) + d; chkmax(r, 0);
			n -= (ql + 1) * d, m -= ql;
			int p = (m - 1) / d;
			for (int i = l; i <= r; i++)
				if (i == 1) s[++j] = 'B';
				else if (i <= 1 + n - p) s[++j] = 'A';
				else if ((n + m - i + 1) % (d + 1) == 0) s[++j] = 'A';
				else s[++j] = 'B';
		}
		if (flg) {
			reverse(s + 1, s + len + 1);
			for (int i = 1; i <= len; i++)
				if (s[i] == 'A') s[i] = 'B';
				else s[i] = 'A';
		}
		s[len + 1] = 0;
		printf("%s\n", s + 1);
	}
	return 0;
}

Problem E. Encoding Subsets

首先,考虑对于一个确定的串,如何计算合法的表示数。

显然可以通过区间 dp 来计算,即记 f i , j f_{i,j} fi,j 表示 S i … j S_{i\dots j} Sij 合法的表示数,转移时考虑 i i i 在答案中单独出现,还是被缩写了即可。

那么,直接套用上述做法,不难得到一个将当前字符串直接作为状态的搜索算法。

这个搜索算法是可以通过的,事实证明, N = 100 N=100 N=100 时,至多只会出现大约 50000 50000 50000 个不同的串。

如何分析这一点呢?考虑将状态串分为两类,一类长度在 12 12 12 以内,另一类长度在 12 12 12 以上。

长度在 12 12 12 以内的串显然只有 2 12 2^{12} 212 个。

对于一个固定的长度 x x x ,考虑如何获得一个长度为 x x x 的状态串,从初始串开始,我们会不断地执行如下操作:
( 1 ) (1) (1) 、选择当前串的一个子串。
( 2 ) (2) (2) 、选择一个当前串长度的因数 k   ( k ≥ 2 ) k\ (k\geq2) k (k2) ,将当前串缩写为 1 k \frac{1}{k} k1

由于 13 × 2 3 > 100 13\times2^3>100 13×23>100 ,因此任意一个长度在 12 12 12 以上的字符串只可能经历了两次缩写,因此对于一个固定的长度 x x x ,至多只有 O ( N 2 ) O(N^2) O(N2) 个状态串。

因此当 N ≤ 100 N\leq100 N100 时,状态数存在下界 O ( N 3 + 2 12 ) O(N^3+2^{12}) O(N3+212)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;
const int P = 998244353;
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("");
}
map <string, int> dp;
void update(int &x, int y) {
	x += y;
	if (x >= P) x -= P;
}
int solve(string s) {
	if (dp.count(s)) return dp[s];
	int &ans = dp[s]; ans = 0;
	if (s.size() == 0) return ans = 1;
	if (s.size() == 1) return ans = s[0] - '0' + 1;
	int len = s.size(); string x, y; x.push_back(s[0]);
	for (int i = 1; i < len; i++) y.push_back(s[i]);
	update(ans, 1ll * solve(x) * solve(y) % P);
	for (int i = 1; i <= len / 2; i++) {
		string x; for (int j = 1; j <= i; j++)
			x.push_back(s[j - 1]);
		for (int j = i + 1, k = 2 * i; k <= len; j += i, k += i) {
			for (int l = j; l <= k; l++)
				chkmin(x[l - j], s[l - 1]);
			string y; for (int l = k + 1; l <= len; l++)
				y.push_back(s[l - 1]);
			update(ans, 1ll * solve(x) * solve(y) % P);
		}
	}
	return ans;
}
int main() {
	string s; cin >> s;
	writeln(solve(s));
	return 0;
}

Problem F. Arcs on a Circle

在圆弧上标上坐标,并固定最长的一段圆弧 x x x [ 0 , L x ] [0,L_x] [0,Lx] 处。

X i X_i Xi 为圆弧 i i i 左端点的坐标,并令 P i P_i Pi F i F_i Fi 分别为 X i X_i Xi 的整数和小数部分,可以看做 P i P_i Pi F i F_i Fi 分别进行等概率随机。

关键的一点观察是:我们并不关心 F i F_i Fi 的具体大小,而只关心 F i F_i Fi 之间的大小关系,因此,我们可以枚举全部的 O ( ( N − 1 ) ! ) O((N-1)!) O((N1)!) 种大小关系。注意我们可以认为存在两个元素相同的概率为 0 0 0 ,并且,这 O ( ( N − 1 ) ! ) O((N-1)!) O((N1)!) 种大小关系是等概率出现的。

完成了枚举之后,问题就变成了离散的概率问题,可以通过传统的动态规划解决,即计算 P i P_i Pi 的选取使得圆弧被完全覆盖的概率。

只需要记 f i , j , S f_{i,j,S} fi,j,S 表示当前处理到坐标 i i i ,最远覆盖到坐标 j j j ,已经放置左端点的圆弧集合为 S S S 的状态出现的概率,转移就比较显然了。

时间复杂度 O ( ( N − 1 ) ! × ( N × C ) 2 × 2 N − 1 ) O((N-1)!\times (N\times C)^2\times 2^{N-1}) O((N1)!×(N×C)2×2N1)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 305;
const int MAXLOG = 64;
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("");
}
bool vis[MAXN];
int n, c, a[MAXN], f[MAXN], h[MAXN], bit[MAXN];
double ans, cnt, dp[MAXN][MAXN][MAXLOG];
void work(int pos) {
	if (pos == n + 1) {
		cnt += 1, memset(dp, 0, sizeof(dp));
		dp[0][a[1]][(bit[n] << 1) - 1] = 1;
		for (int i = 0; i <= c - 1; i++)
		for (int j = i; j <= c; j++)
		for (int s = 0; s <= (bit[n] << 1) - 1; s++) {
			int pos = h[i % n];
			if (j != i) dp[i + 1][j][s] += dp[i][j][s];
			if (bit[pos] & s) dp[i + 1][min(max(j, i + a[pos]), c)][s ^ bit[pos]] += dp[i][j][s] / (c / n);
		}
		ans += dp[c][c][0];
		return;
	}
	for (int i = 1; i <= n - 1; i++)
		if (!vis[i]) {
			f[pos] = i;
			h[i] = pos;
			vis[i] = true;
			work(pos + 1);
			vis[i] = false;
		}
}
int main() {
	read(n), read(c), c *= n;
	for (int i = 1; i <= n; i++)
		read(a[i]), a[i] *= n;
	for (int i = 2; i <= n; i++)
		bit[i] = 1 << (i - 2);
	sort(a + 1, a + n + 1, [&] (int x, int y) {return x > y; });
	work(2), printf("%.15lf\n", ans / cnt);
	return 0;
}

Atcoder Grand Contest 21

Problem E. Ball Eat Chameleons

首先,考虑对于给定的颜色序列,如何正确地投喂变色龙。

由于我们需要使得 N N N 只变色龙变红,因此,应当尽可能地将较早出现的红球喂给还没有吃过任何球的变色龙。对于蓝球,如果当前有吃过一个红球的变色龙,则可以直接喂给它,否则,可以考虑喂给最后一只变色龙,让它吃下所有其余的球。

该策略显然是最优的,考虑枚举红球个数 X X X ,蓝球个数 Y Y Y ,令 d = X − Y d=X-Y d=XY ,如何对能够全部变红的颜色序列计数。

对于 X < Y X<Y X<Y X < N X<N X<N 的情况,答案显然为 0 0 0

对于 X = Y X=Y X=Y 的情况,为了最后一只变色龙变红,我们需要最后一个球颜色为蓝,且被喂给了最后一只变色龙;并且,前面的所有 N − 1 N-1 N1 只变色龙必须在吃下一个红球后再吃下一个蓝球。即最早出现的 N N N 个红球都要能够匹配靠后的一个蓝球。
将红球视为 + 1 +1 +1 ,蓝球视为 − 1 -1 1 ,那么,最早出现的 N N N 个红球都能匹配靠后的一个蓝球等价于序列的后缀和始终不超过 X − N X-N XN 。可以用对称法对这样的序列进行计数,时间复杂度为 O ( 1 ) O(1) O(1)

对于 X > Y X>Y X>Y 的情况,最后一个球颜色不一定要为蓝,可以考虑枚举最后一个球的颜色,分两种情况考虑后,可以发现最终我们只需要序列的后缀和始终不超过 X − N + D X-N+D XN+D 就可以了。

时间复杂度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值