AtCoder Beginner Contest 346

最刺激的一集。

尝试挑战手速极限,用了 57s 做 A。但是好像还是很慢。

然后做 B,仍然想挑战手速。结果一眼出思路只要把 wbwbwwbwbwbw 多重复几遍就可以代替「无限长」。

很快就写完了。然后交了三发罚时。后来发现我复制若干遍 wbwbwwbwbwbw 的时候好像复制错了/ll /ll

过 B 之前很快过掉了 C, D。其中 D 有一发罚时是因为 DP 的边界错了。

过了 A~D 后看 E。感觉没什么思路。但是想到了如果这一行/列被覆盖了多次,那么只有最后一次覆盖是有效的。

于是想先实现这个过程。发现如果要这么做,必然需要将所有操作倒序处理。于是就莫名奇妙地联想到了白雪皑皑,发现倒序处理是正确的。延续着涂色的思路打出了正解。

此时是 43min。还有一个小时。

看 F 题。发现太典了。题目中字字句句都在提示二分,而二分的 check 又是一个入门贪心。想了一两分钟就开始写了。

越写越觉得 check 太过麻烦,但是还是硬着头皮写下来了份 140+ 的代码。没有调试,发现比答案少一。加上一后直接交了。结果 AC15 WA35。简单修改变成了 AC26 WA24。

然后发现加一绝对是错误的。去掉后开始疯狂调试 + 补充代码。赛时我的代码中写了若干个二分,没有把二分封装成函数,结果赛后发现每个二分都写错了。除此之外小错误和恶心边界比比皆是,写着写着就有些崩溃了。

于是硬生生地冲着 CP Editor 写了一个小时。比赛结束。

这份代码已经丑陋到 170+ 了。想了想还是重构代码吧。赛后一小时终于过了。

这里列几个犯的错误:

int getp(int l, int r, int k, int w) {		// [l, r] 中第一个 p 使得 [l, p] 中 w 出现 k 次
    int pos = -1, L = l;		// 要把最开始的 l 记录下来
	while (l <= r) {
        int mid = l + r >> 1, S = calc(L/*这里不能用 l,需要用最开始的 L*/ , mid, w);
        // if (S == k) return mid;	是错误的。
		if (S >= k) pos = mid, r = mid - 1;
		else l = mid + 1;
    }
	return pos;
}

bool chk(ll k) {
	pair<ll, int> cur = {1, 0};
	for (int i = 1; i <= lb; ++ i ) {
		cur = nxt(cur.first, cur.second, b[i] - 'a', k);
		if (cur.first > n) return false;// 如果不写这行,在最后写 return cur.first <= n,那么 cur.first 会超出 long long 的范围
	}
	return true;
}

D - Gomamayo Sequence

  • 给定一个长度为 \(n\)\(01\)\(s\)
  • 如果 \(s\) 是「IOIAKer」,当且仅当满足以下条件:
    • 有且仅有一个整数 \(i\) 满足 \(1 \le i < n\)\(s_i = s_{i + 1}\)
  • 对于每个 \(i = (1, 2, 3, \dots, n)\),执行以下操作:
    • 花费 \(c_i\) 的代价将 \(s_i\) 取反。
  • 求将 \(s\) 变成「IOIAKer」所需的最小代价。
  • \(n \le 2 \times 10^5\)\(c_i \le 10^9\)

一眼 DP。

设状态 \(f_{i, 0/1, 0/1}\) 表示若只考虑 \(s\) 的前 \(i\) 位,第 \(i\) 位将要变成 \(0/1\),且字符串将要变成状态 \(0/1\) 所需要花费的最小代价。其中,「状态 \(0\)」表示 \(01\) 交替,即不存在 \(s_i = s_{i + 1}\)。「状态 \(1\)」表示题目中的要求,即存在一个 \(s_i = s_{i + 1}\)。那么答案即 \(\min(f_{n, 0, 1}, f_{n, 1, 1})\)

转移可以枚举上一位填的什么。然后根据是否需要翻转计算代价。

f[i][0][0] = f[i - 1][1][0] + (s[i] == '1') * c[i];
f[i][1][0] = f[i - 1][0][0] + (s[i] == '0') * c[i];
f[i][0][1] = min(f[i - 1][1][1], f[i - 1][0][0]) + (s[i] == '1') * c[i];
f[i][1][1] = min(f[i - 1][0][1], f[i - 1][1][0]) + (s[i] == '0') * c[i];
$\color{blue}\text{Code}$
int n, c[N];
char s[N];
int f[N][2][2];		// 将前 i 个位置变成 0101010...,

void Luogu_UID_748509() {
	scanf("%lld%s", &n, s + 1);
	for (int i = 1; i <= n; ++ i ) scanf("%lld", c + i);
	f[2][0][0] = (s[1] == '0') * c[1] + (s[2] == '1') * c[2];
	f[2][1][0] = (s[1] == '1') * c[1] + (s[2] == '0') * c[2];
	f[2][0][1] = (s[1] == '1') * c[1] + (s[2] == '1') * c[2];
	f[2][1][1] = (s[1] == '0') * c[1] + (s[2] == '0') * c[2];
	for (int i = 3; i <= n; ++ i ) {
		f[i][0][0] = f[i - 1][1][0] + (s[i] == '1') * c[i];
		f[i][1][0] = f[i - 1][0][0] + (s[i] == '0') * c[i];
		f[i][0][1] = min(f[i - 1][1][1], f[i - 1][0][0]) + (s[i] == '1') * c[i];
		f[i][1][1] = min(f[i - 1][0][1], f[i - 1][1][0]) + (s[i] == '0') * c[i];
	}
	fout << min(f[n][0][1], f[n][1][1]) << '\n';
}

E - Paint

  • 有一个 \(H \times W\) 的网格,最开始每个格子的颜色都是 \(0\)
  • 按顺序执行 \(N\) 个操作,每个操作形如 \((T_i, A_i, X_i)\)
    • \(T_i = 1\),将第 \(A_i\) 行全部涂成 \(X_i\) 颜色。
    • \(T_i = 2\),将第 \(A_i\) 列全部涂成 \(X_i\) 颜色。
  • 求最后每种颜色的出现次数。
  • \(1 \le H, W, M, X_i \le 2 \times 10^5\)

首先观察到,如果对某一行/列进行了多次染色,那么只有最后一次染色是有效的,所以我们只保留最后一次染色。如果这一行/列自始至终没有没染过色,相当于最开始为它全染成 \(0\)

这样,我们就相当于对于每一行和每一列都进行了恰好一次染色操作。那么做法是将所有操作倒序处理。

假如在「在第 \(1\) 列染颜色 \(2\)」之后,总共出现过 \(k\) 次染整行的操作,那么最终第 \(1\) 列上颜色 \(2\) 会有 \(n - k\) 个。行列翻转过来同理。

所以我们在倒序处理操作的过程中,记录已经操作过多少次行/列,并分解计算即可。

$\color{blue}\text{Code}$
int n, m, k;
int nn, mm;
int res[N];

struct Node {
	int op, a, x;
}a[N];

void Luogu_UID_748509() {
	fin >> n >> m >> k;
	for (int i = 1; i <= k; ++ i ) {
		fin >> a[i].op >> a[i].a >> a[i].x;
	}
	
	map<pair<int, int>, bool> mp;
	auto work = [&](int op, int a, int x) {
		pair<int, int> t = {op, a};
		if (mp.count(t)) {
			return;
		}
		mp[t] = true;
		
		if (op == 1) {
			res[x] += m - nn;
			++ mm;
		}
		else {
			res[x] += n - mm;
			++ nn;
		}
	};
	
	for (int i = k; i; -- i ) work(a[i].op, a[i].a, a[i].x);
	for (int i = 1; i <= n; ++ i ) work(1, i, 0);
	
	vector<pair<int, int> > ans;
	for (int i = 0; i <= 200000; ++ i )
		if (res[i]) ans.push_back({i, res[i]});
	fout << ans.size() << '\n';
	for (auto t : ans) {
		fout << t.first << ' ' << t.second << '\n';
	}
}

F - SSttrriinngg in StringString

  • 对于一个字符串 \(X\) 和一个非负整数 \(k\),定义 \(f(X, k)\) 表示将 \(X\) 重复 \(k\) 次得到的字符串,\(g(X, k)\) 表示将 \(X\) 的每个字符重复 \(k\) 次得到的字符串。例如当 \(X = \texttt{abc}\) 时,\(f(X, 2) = \texttt{abcabc}, g(X, 3) = \texttt{aaabbbccc}\)。特别的,当 \(k = 0\) 时,\(f(X, k), g(X, k)\) 为空串。
  • 给定两个字符串 \(S, T\) 和一个正整数 \(n\)。求最大的非负整数 \(k\) 满足 \(g(T, k)\)\(f(S, n)\) 的子序列。
  • \(1 \le n \le 10^{12}\)\(1 \le |S|, |T| \le 10^5\)

子序列是具有传递性的。对于字符串 \(s_1, s_2, s_3\) 而言,如果 \(s_1\)\(s_2\) 的子序列,\(s_2\)\(s_3\) 的子序列,那么 \(s_1\)\(s_3\) 的子序列。

容易发现 \(g(T, k)\) 一定是 \(g(T, k + 1)\) 的子序列。归纳可以证明,如果答案为 \(k\),那么所有 \(1 \le i \le k\) 都满足 \(g(T, i)\)\(f(S, n)\) 的子序列。这启发我们可以二分答案。

问题就转化成了,已知 \(S, T, k, n\),判断 \(g(T, k)\) 是否是 \(f(S, n)\) 的子序列。而对于这个问题,我们又可以计算最小的 \(m\) 使得 \(g(T, k)\)\(f(S, m)\) 的子序列,然后判断是否 \(m \le n\)

所以我们的任务是求解 \(m\)

这是一个简单贪心。在判断「字符串 \(a\) 是否是字符串 \(b\) 的子序列」时,我们逐位考虑 \(a_i\)\(b\) 中的哪个字符匹配。如果 \(a_i\)\(b_{pos_i}\) 匹配,那么在匹配 \(a_{i + 1}\) 时,\(pos_{i + 1}\) 应该是最靠前的 \(p \in (pos_i, |b|]\) 使得 \(a_{i + 1} = b_p\)

同理,我们想逐位考虑 \(g(T, k)_i\) 但是是不现实的,因为 \(k\) 太大了。所以我们逐位考虑 \(T_i\),然后计算 \(k\)\(T_i\) 后,匹配的位置在哪里。这是不难实现的。

定义 \(x, y\) 表示当前在第 \(x\)\(S\) 的第 \(y\) 个位置,最开始 \(x = 1, y = 0\)。然后逐位考虑 \(T_i\),调整 \(x, y\)。最终 \(x\) 即所求的 \(m\)

上述「调整」过程大致可分三步:

  • 将第 \(x\)\(S\) 的位置 \([y + 1, |S|]\) 匹配完;
  • 匹配若干个完整的 \(S\)
  • \(T\) 中没匹配完的字符从头与 \(S\) 匹配。

实现上,我们需要维护 \(sum_{i, j}\) 表示 \(S_{i \sim |S|}\) 中字符 \(j\) 的出现次数。进行上述三个操作时需要时刻计算这个操作结束后是否有下一个操作。

$\color{blue}\text{Code}$
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const int N = 1e6 + 10;

ll n;
int la, lb;
char a[N], b[N];
int sum[N][26];
int lst[26];

int calc(int l, int r, int w) {
	return sum[l][w] - sum[r + 1][w];
}

int getp(int l, int r, int k, int w) {
	int pos = -1, L = l;
	while (l <= r) {
		int mid = l + r >> 1, S = calc(L, mid, w);
		if (S >= k) pos = mid, r = mid - 1;
		else l = mid + 1;
	}
	return pos;
}

pair<ll, int> nxt(ll x, int y, int w, ll k) {
	if (sum[y + 1][w] >= k) return make_pair(x, getp(y + 1, la, k, w));
	k -= sum[y + 1][w], x ++ , y = 0;
	
	if (k % sum[1][w] == 0) return make_pair(x + k / sum[1][w] - 1, lst[w]);
	x += k / sum[1][w], y = 0, k %= sum[1][w];
	
	return make_pair(x, getp(1, la, k, w));
}

bool chk(ll k) {
	pair<ll, int> cur = {1, 0};
	for (int i = 1; i <= lb; ++ i ) {
		cur = nxt(cur.first, cur.second, b[i] - 'a', k);
		if (cur.first > n) return false;
	}
	return true;
}

signed main() {
	scanf("%lld%s%s", &n, a + 1, b + 1);
	la = strlen(a + 1), lb = strlen(b + 1);
	
	for (int i = la; ~i; -- i ) {
		for (int j = 0; j < 26; ++ j ) sum[i][j] = sum[i + 1][j];
		if (i) sum[i][a[i] - 'a'] ++ ;
	}
	
	for (int i = 1; i <= la; ++ i ) lst[a[i] - 'a'] = i;
	
	for (int i = 1; i <= lb; ++ i )
		if (!sum[1][b[i] - 'a']) return puts("0"), 0;
	
	ll l = 1, r = 1e18, res = 0;
	while (l <= r) {
		ll mid = l + r >> 1;
		if (chk(mid)) res = mid, l = mid + 1;
		else r = mid - 1;
	}
	
	printf("%lld\n", res);
	return 0;
}
posted @ 2024-03-23 23:30  2huk  阅读(88)  评论(0编辑  收藏  举报