涉及知识点:模拟、贪心、枚举、递归、动态规划、AC自动机、组合计数、容斥原理
A. String Similarity
不能再水的送分题?有挺多个做法。
首先我们观察到每个要求都包含 (s_n),那我们把所有 (w_i) 都搞成 (s_n) 就一定满足要求。
#include <bits/stdc++.h>
using namespace std;
int t;
int n;
char s[200];
int main() {
scanf("%d", &t);
while (t--) {
scanf("%d", &n);
scanf("%s", s + 1);
for (int i = 1; i <= n; i++) printf("%d", s[n] - '0');
printf("
");
}
return 0;
}
还有其它的构造方法,比如:(w_i=s_{2i-1})。这样倒过来构造也对。
B - RPG Protagonist
直接贪心是不对的,我们观察题目性质,发现 (cnt_s) 很小,这就在提示我们枚举。
有一个贪心还是对的,就是钱少的那个显然买的越多越好。我们不妨设 S 是钱少的那个。
那么我们可以枚举第一个人买了多少 S,这样第二个买了多少也就知道,他们分别还剩多少钱也就清楚了,很容易算出还可以买多少 W。
#include <bits/stdc++.h>
using namespace std;
int t;
int p, f;
int cs, cw;
int s, w;
int ans;
int main() {
cin >> t;
while (t--) {
ans = 0;
cin >> p >> f;
cin >> cs >> cw;
cin >> s >> w;
if (s > w) swap(s, w), swap(cs, cw);
int i = min(p / s + f / s, cs);
for (int j = 0; j <= i && s * j <= p; j++) {
int tmpp = p - s * j;
int tmpf = f - s * (i - j);
if (tmpf < 0) continue;
ans = max(ans, i + min(tmpp / w + tmpf / w, cw));
}
cout << ans << "
";
}
return 0;
}
C - Binary String Reconstruction
一道不错的模拟题,可是放在2C这个位置是不是有点水?
首先我们显然可以确定 (w_i) 是不是 (0),因为如果 (s_{i+x}=0) 或 (s_{i-x}=0) 则 (w_i) 一定是 (0)。否则令 (w_i=1)。
再判断一下是否有 (s_i=1) 但是 (w_{i-x}=0) 且 (w_{i+x}=0) 的情况,如果出现则答案为 (-1)。出现这种情况的原因是 (s_{i-2x}=0) 且 (s_{i+2x}=0)。
#include <bits/stdc++.h>
using namespace std;
int t;
int n, x;
char s[100010], w[100010], ans[100010];
int main() {
cin >> t;
while (t--) {
scanf("%s", s + 1);
cin >> x;
n = strlen(s + 1);
for (int i = 1; i <= n; i++) {
if (i - x > 0 && s[i - x] == '0') w[i] = '0';
else if (i + x <= n && s[i + x] == '0') w[i] = '0';
else w[i] = '1';
}
bool flag = 1;
for (int i = 1; i <= n; i++) {
if (s[i] == '1') {
int cnt = 0;
if (i + x <= n && w[i + x] != '0') cnt++;
if (i - x > 0 && w[i - x] != '0') cnt++;
if (!cnt) flag = 0;
}
}
if (!flag) {
puts("-1");
continue;
}
for (int i = 1; i <= n; i++) cout << w[i];
cout << "
";
}
return 0;
}
D - Zigzags
看到数据范围很容易想到枚举两个位置。关键是枚举哪两个位置,其实枚举任意两个位置都是可的但是代码复杂度各不相同。
我是尝试枚举 (i,k),同时我们统计每个位置之后的每种数有多少个,记为 (cnt_{i,v})。
然后开始枚举,先枚举 (i),然后枚举 (k),在扩展的时候记录 (i ightarrow k) 这一段内每种数字的个数,记作 (num_v),每扩展一个 (k),我们需要知道在 ((i,k)) 和 ((k,n]) 范围内的共同数字的个数,记为 (cur)。那么显然数字 (a_k) 和之前的都不能算了,所以要减去 (num_{a_k})。如果 (a_i=a_k) 则更新答案。接下来又多了个 (a_k),令 (cur+=cnt[k][a[k]]),同时 (num_{a_k}) 要自增 (1)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template <typename T> void read(T &x) {
T f = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for(x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
x *= f;
}
int t;
int n;
int a[3010], cnt[3010][3010], num[3010];
ll ans;
int main() {
read(t);
while (t--) {
ans = 0;
read(n);
for (int i = 1; i <= n; i++) read(a[i]);
for (int i = 1; i <= n + 5; i++) for (int j = 1; j <= n + 5; j++) cnt[i][j] = 0;
for (int i = n - 1; i >= 1; i--) {
for (int j = 1; j <= n; j++) cnt[i][j] = cnt[i + 1][j];
cnt[i][a[i + 1]]++;
}
for (int i = 1; i <= n; i++) {
ll cur = 0;
for (int j = i + 1; j <= n; j++) {
cur -= num[a[j]];
if (a[i] == a[j]) ans += cur;
cur += cnt[j][a[j]];
num[a[j]]++;
}
for (int j = i + 1; j <= n; j++) num[a[j]]--;
}
printf("%lld
", ans);
}
return 0;
}
赛后发现枚举 (j,k) 要简单好多。不过我相信枚举 (j,k) 的题解会很多,我这篇题解只是给枚举 (i,k) 的但是不知道自己哪里错了的人查错的。另外这也是一种思路。
E - Clear the Multiset
老套路题了,这种题貌似真的很多。考虑一个区间,我们有两种消的办法:
- 全部用方法二
- 先用方法一消到最小值,然后递归去求
肯定不会有人脑抽先用几次方法一再用方法二吧。。。
那么直接按照上面的模拟就好。因为区间不会重复,所以不需要记忆化。
对于第二种消法,我们只需找到一个最小值,根据这个去分区间就行。然后递归求解时记得加一个当前已被消了多少。关于时间复杂度,我们可以把这个递归过程看做一棵树,这个树最多有 (n) 层,每层以为区间不重叠所以是 (O(n)) 的,总的时间复杂度即为 (O(n^2))。还可以通过 rmq,笛卡尔树等做到 (O(nlog{n})),(O(n))。
#include <bits/stdc++.h>
using namespace std;
int n;
int a[5010];
int solve(int l, int r, int lst) {
if (l > r) return 0;
if (l == r) return a[l] > lst;
int mi = l;
for (int i = l + 1; i <= r; i++) if (a[i] < a[mi]) mi = i;
return min(r - l + 1, solve(l, mi - 1, a[mi]) + solve(mi + 1, r, a[mi]) + a[mi] - lst);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
cout << solve(1, n, 0);
return 0;
}
F - x-prime Substrings
AC自动机好题。赛时我一直以为是前缀和乱搞,赛后看题解发现是AC自动机觉得很妙。
观察到 (x) 很小,我们跑一遍暴力发现所有 x-prime 数的长度的和最大也不超过 (5000)(实测应该是 (x=19) 时最大)。那问题就变成了总长度不超过 (5000) 的模式串和一个文本串问最少从文本串中删去几个字符可以使得文本串不包含任意一个模式串。
多串匹配问题想到AC自动机。这种问题又可以想到动态规划。设 (dp_{i,j}) 代表匹配到文本串第 (i) 位且当前在AC自动机上的状态 (j)。首先下个位置肯定可以删掉,所以 (dp_{i+1,j}) 可以为 (dp_{i,j}+1)。然后考虑不删下一个字符,则加上下一个字符后一定不能是某个模式串结尾,这可以在 (fail) 树上 (dp) 求得某个状态是不是某个模式串的结尾。如果不是,则其可以为 (dp_{i,j})。最后的答案就是 (min(dp_{n,所有状态}))。
#include <bits/stdc++.h>
using namespace std;
char s[1010];
int n, x;
int t[25], cnt;
int trie[5010][10], tot = 1;
int fail[5010];
queue<int> q;
int dp[1010][5010];
bool End[5010];
bool check() {
for (int i = 1; i <= cnt; i++) {
int now = 0;
for (int j = i; j <= cnt; j++) {
if (i == 1 && j == cnt) continue;
now += t[j];
if (x % now == 0) return false;
}
}
return true;
}
void dfs(int now) {
if (now == 0) {
if (!check()) return;
int p = 1;
for (int i = 1; i <= cnt; i++) {
if (!trie[p][t[i]]) trie[p][t[i]] = ++tot;
p = trie[p][t[i]];
} End[p] = true;
return;
}
for (int i = 1; i <= 9; i++) {
if (i <= now) {
t[++cnt] = i;
dfs(now - i);
cnt--;
}
}
}
int main() {
scanf("%s%d", s + 1, &x);
n = strlen(s + 1);
dfs(x);
fail[1] = 0;
for (int i = 1; i <= 9; i++) trie[0][i] = 1;
q.push(1);
while (!q.empty()) {
int p = q.front();
q.pop();
End[p] |= End[fail[p]];
for (int i = 1; i <= 9; i++) {
if (trie[p][i]) {
fail[trie[p][i]] = trie[fail[p]][i];
q.push(trie[p][i]);
} else {
trie[p][i] = trie[fail[p]][i];
}
}
}
memset(dp, 0x3f, sizeof(dp));
dp[0][1] = 0;
for (int i = 0; i < n; i++) {
for (int j = 1; j <= tot; j++) {
dp[i + 1][j] = min(dp[i + 1][j], dp[i][j] + 1);
if (!End[trie[j][s[i + 1] - '0']]) {
dp[i + 1][trie[j][s[i + 1] - '0']] = min(dp[i + 1][trie[j][s[i + 1] - '0']], dp[i][j]);
}
}
}
int ans = 0x3f3f3f3f;
for (int i = 1; i <= tot; i++) ans = min(ans, dp[n][i]);
printf("%d", ans);
return 0;
}
G - Mercenaries
2020-08-27
来补了qwq。
这道题正面不好考虑,尝试用容斥原理来做。
首先对于第一个限制是好求的,我们先求出满足第一个限制的个数。考虑枚举个数 (s)。记个数为 (s) 时的候选集合大小为 (cnt_s),那么答案就会加上 (C_{cnt_s}^s)。如何求出 (cnt_s)?我们发现就是求有多少个区间 ([l_i,r_i]) 包含其,那么直接静态区间加,单点求和—差分+前缀和做就好了。
这样子我们得到的全集的大小 (|S|)。答案为 (|S|-|cup_{k=1}^mS_i|),其中 (S_i) 代表 (S) 中同时包含 (a_i,b_i) 的方案。后面的东西容斥做就好了。
发现即为 (sum(-1)^{k-1}sum_{U subseteq M,f(U)=k}sum_{s=L(U)}^{R(U)}C_{cnt_s-k}^{s-k}),其中 (M) 是所有限制二的选择的集合,(f(U)) 代表 (U) 内所有涉及的士兵的个数,(L(U)) 为 (U) 内所有士兵 ([l,r]) 的并集的下界,(R(U)) 即为上界。(C_{cnt_s-k}^{s-k}) 则代表要选 (s) 个士兵,已经选好了 (k) 的士兵,要在剩下 (cnt_s-k) 中选 (s-k) 个。求并集直接把所有的 (l) 取 (max),所有 (r) 取 (min)。我们发现知道了 (L(U),R(U)) 后这个东西和 (U) 没有具体的关系,所以可以预处理。而 (k) 最大其实也就 (2m)。所以我们得到了一个预处理 (O(nm)),查询 (O(2^mm)) 的算法。还有个问题,查询时如何搞出 (U) 内的所有士兵?其实不需要用set
,直接开一个标记数组,最后把打上标记的数再删除标记即可,时间复杂度 (O(m))。另外预处理阶乘及其逆元可能需要 (O(nlog{MOD})) 的时间。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;
const int MAXN = 300010;
ll n, m, a[MAXN], b[MAXN], l[MAXN], r[MAXN], cnt[MAXN], fac[MAXN], ifac[MAXN], vis[MAXN], num[MAXN][41], stk[41], top, ans;
ll ksm(ll x, ll y) {
ll ret = 1;
while (y) {
if (y & 1) ret = (ret * x) % mod;
x = (x * x) % mod;
y >>= 1;
}
return ret;
}
ll C(ll x, ll y) {
if (x < 0 || y < 0 || x < y) return 0;
return fac[x] * ifac[x - y] % mod * ifac[y] % mod;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> l[i] >> r[i], cnt[l[i]]++, cnt[r[i] + 1]--;
for (int i = 1; i <= n; i++) cnt[i] += cnt[i - 1];
for (int i = 1; i <= m; i++) cin >> a[i] >> b[i];
fac[0] = ifac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mod, ifac[i] = ifac[i - 1] * ksm(i, mod - 2) % mod;
for (int i = 1; i <= (m << 1); i++) {
for (int j = 1; j <= n; j++) {
num[j][i] = (num[j - 1][i] + C(cnt[j] - i, j - i)) % mod;
}
}
for (int i = 1; i <= n; i++) ans = (ans + C(cnt[i], i)) % mod;
for (int state = 1, flag; state < (1 << m); state++) {
top = 0;
flag = 1;
for (int i = 0; i < m; i++) {
if ((state >> i) & 1) {
flag *= -1;
if (!vis[a[i + 1]]) stk[++top] = a[i + 1];
if (!vis[b[i + 1]]) stk[++top] = b[i + 1];
vis[a[i + 1]] = vis[b[i + 1]] = 1;
}
}
ll L = l[stk[1]], R = r[stk[1]];
for (int i = 2; i <= top; i++) {
L = max(L, l[stk[i]]);
R = min(R, r[stk[i]]);
}
for (int i = 0; i < m; i++) if ((state >> i) & 1) vis[a[i + 1]] = vis[b[i + 1]] = 0;
if (L <= R) ans = (ans + flag * (num[R][top] - num[L - 1][top]) + mod) % mod;
}
cout << ans << "
";
return 0;
}