闲话
关于我的博客…… 我该写还是会写的 而且应该不会少
博客日更大概只有在改不出模拟赛的题时才会断
而且最近写洛谷题解勤了些 所以最近博客还长了点(
关于为什么要写……学 oi 本来就没什么需要藏私的
你写的东西如果别人能看懂 那他们总能自己在看别人的博客时或自己想明白的
学这个的没人是纯傻子
更多的时候因为写的是形式化的东西 所以本身就不是为了“带着别人思考”
题解这东西更多的只是指引一个方向,真正的思考是少不了的
有没有这一篇博客没有任何区别
看我博客的似乎没有什么外校人
更多的还是自己学校的或者有过 oi 上的交流的
这种的都可以说是“同学”了
自己也总会去看看别人的博客
以及我说到最近在写洛谷题解
同时也在翻译题面和题解
最近写了六七篇了已经
写题解这件事最大的还是对自己有帮助 如果啥帮助也没有那我还不如不写
写题解能帮自己理顺做题的思路 自己也只有在“能讲出来”之后才可以说真正会了这题
当然在写的时候肯定会有表述不出来的时候和细节问题
这些其实才是写题解最大的收获 写出来后对这道题的理解就能上升一层
更好的帮助自己提升
完了再写下去就要开始语文作文了 所以就在这收尾吧
总而言之,写题解是不会停止的
同时也不会加密码啥的 大家能共同提升才算真正的提升
写得很长……好久没写那么长的闲话了
发现也只有心灵受到触动时才能写出来啊
杂题
一块黑板上写着 \(n\) 个整数。第 \(i\) 个整数记作 \(a_i\)。保证 \(n\) 是偶数。此外,给定 \(L,R\)。
Alice 和 Bob 在玩一个游戏。他们轮流操作,Alice 先手。在每一轮中,玩家需要选择一个写在黑板上的数,并擦掉它。
游戏会在 \(n\) 轮后结束。令 \(s\) 为 Alice 擦掉的数之和。若 \(L \le s \le R\),则 Alice 胜利,反之 Bob 胜利。你需要输出当两方都采取最优策略情况下的赢家。
\(2\le n\le 5000,\ 1\le a_i\le 10^9, 0\le L \le R \le \sum_{1\le i\le n} a_i\)。
记 \(S = \sum_{1\le i\le n} a_i\),\(S_A\) 为 Alice 擦掉的数之和,\(S_B\) 为 Bob 擦掉的数之和。
记一个长为 \(m\) 序列 \(b_i\) 的邻项差分 \(\text{diff }b = (b_2 - b_1) + (b_4 - b_3) + \cdots + (b_m - b_{m-1})\)。
首先转化题意。条件 \(L \le S_A \le R\) 可以通过整体乘 \(2\) 减 \(S\) 转化为 \(2L - S \le S_A - S_B \le 2R - S\)。随后设 \(x = S - (L + R)\),整体加 \(x\) 后变为 \(L - R \le x + S_A - S_B \le R - L\),即 \(|x + S_A - S_B| \le R - L\)。
因此有转化后的问题:
给定整数 \(x\)。两人轮流操作,每次选择一个没有选过的数字 \(a_i\)。在 Alice 的回合,置 \(x=x + a_i\),而在 Bob 的回合置 \(x=x - a_i\)。最终的分数即为 \(x\) 的绝对值。Alice 的目标是最小化这个值,而 Bob 的目标是最大化它。求当两方都采取最优策略情况下的赢家。
不妨假设 \(a_i\) 升序排列。我们断言,最终的分数可以通过如下方式求得:
- 选择整数 \(p\),将 \(p, p + x, a_1, a_2,\cdots, a_n\) 升序排列得到 \(A_i\)。令 \(p\) 的答案为 \(\text{diff }A\) 的值。
- 最终的分数即为所有可能的 \(p\) 的答案中的最小值。
容易发现 \(p\) 所有对答案有贡献的取值为 \(p = a_i\)。对于 \(p\) 的某一确定取值可以在 \(O(n)\) 的时间复杂度内计算答案,取其中最小值作为最终分数即可。
因此有总时间复杂度 \(O(n^2)\) 的算法。
证明:
可以通过如下方式计算分数:Alice 操作后,黑板上剩余 \(n-1\) 个整数。将目前可选的 \(n-1\) 个整数与目前的 \(x\) 一同进行升序排序,得到序列 \(b_i\)。Bob 操作后的分数即为 \(\text{diff }b\)。
使用数学归纳法证明该方式的正确性。
当 \(n=2\) 时正确性显然。
假设已经证明了对 Bob 来说 \(n=k\) 的情况是成立的,现在需要证明对 Alice 来说 \(n=k + 1\) 的情况也是成立的。
Alice 的目标等价于将其中一个 \(a_i\) 更换为 \(a_i + x\),使得 Bob 的分值最小。选择 \(a_i\) 对应着选择 \(p = a_i\),这样的策略肯定能够产生可能得到的分数中最小的值,因此对 Alice 来说 \(n=k + 1\) 的情况通过该方式计算是正确的。
假设已经证明了对 Alice 来说 \(n=k + 1\) 的情况是成立的,现在需要证明对 Bob 来说 \(n=k+2\) 的情况也是成立的。
我们将 Bob 可选的 \(n-1\) 个整数与目前的 \(x\) 一同进行升序排序,得到序列 \(b_i\)。令 \(b_s = x\)。如果 Bob 在本轮中选择了 \(b_t(s \neq t)\),那么 Alice 可以选择 \(p=b_t\) 来得到分数 \(\text{diff }b\)。因此该值是分数的一个上界。
这个上界是可以取到的。不妨假设 \(s\) 是奇数。如果 Bob 在 \(s=1\) 时选择 \(t=n\),在其余时刻选择 \(t = s-1\),最终的分数就是该值。当 \(s\) 为偶数的时候类似:当 \(s=n\) 时选择 \(t=1\),在其余时刻选择 \(t =s+1\) 即可。
根据数学归纳法,原断言成立。
总时间复杂度 \(O(n^2)\)。
code
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i,s,t) for (register int i = (s), i##_ = (t) + 1; i < i##_; ++ i)
int n, l, r, a[5005], s;
vector<int> ans;
signed main() {
cin >> n >> l >> r;
rep(i,1,n) cin >> a[i], s += a[i]; s -= l + r;
sort(a + 1, a + 1 + n);
int ret = LLONG_MAX, now;
rep(i,1,n) {
ans.clear(); now = 0;
rep(j,1,n) if (i != j) ans.emplace_back(a[j]);
ans.insert(lower_bound(ans.begin(), ans.end(), a[i] + s), a[i] + s);
for (int i = 0; i < ans.size(); i += 2) now += ans[i + 1] - ans[i];
ret = min(ret, now);
} puts(abs(ret) <= r - l ? "Alice" : "Bob");
}
这场的 E 是 3877,F 是 4310
我不认为短期内我可以做到理解题解
所以先咕着
然后写了 AGC050
A 是个倍增,没什么好说的
有 \(n\) 个盒子排成一列,从左到右分别编号为 \(1\sim n\)。
最开始,每个盒子都是空的。你可以以任何顺序进行如下两种操作任意次:
- 选择三个连续的空盒子,向其中分别放入一枚硬币。
- 选择三个连续的放有硬币的盒子,取出其中的硬币。
操作完后,假设第 \(i\) 个盒子中装有硬币,你会获得 \(a_i\) 分。最终分数是你从各个盒子处获得的分数的和。
请输出最终分数的最大值。
\(3\le n\le 500, \ -100\le a_i \le 100\)。
给定一系列位置 \(x_i\),我们能否知道是否可以通过一系列操作使得只有这些位置的盒子内装有硬币?答案是肯定的。
首先我们发现,我们只需要关注 \(x_i\bmod 3\) 的值。这是因为我们可以通过进行如下的操作使得 \(x_i\) 左右平移三个位置:
...o
\(\to\) oooo
\(\to\) o...
然后有了转化后的题意:
给定一个初始为空的串和目标串。你可以以任何顺序进行如下两种操作任意次:
- 在串的任意位置加入
012
、120
、201
中的一种。 - 在串的任意位置移除形如
012
、120
、201
的子串。
询问是否能够构造出目标串。
可以证明一定不需要移除操作。
如果移除操作和上一次加入操作的操作范围有交集,则这两次操作互相抵消。反之我们一定可以通过不断交换移除操作和上一次加入操作的顺序,从而最终转化到前一种情况。容易证明不存在其他情况。
因此我们只需要着眼于加入子串能构造出的所有串。我们可以递归地定义可以构造出的串:
- 空串是可以构造出的。
- 设串长度为 \(n\)。如果存在三个位置 \(x_i < x_j < x_k\),满足 \(x_j\equiv x_i + 1\pmod 3\)、\(x_k\equiv x_j + 1\pmod 3\),且子串 \([1,i)\)、\((i,j)\)、\((j,k),(k,n]\) 都是可以构造出的串,则该串是可以构造出的。
这给出了一种区间 dp 的方法。总时间复杂度 \(O(n^3)\)。
code
#include <bits/stdc++.h>
using namespace std;
#define rep(i,s,t) for (register int i = (s), i##_ = (t) + 1; i < i##_; ++ i)
const int N = 500 + 10;
int n, a[N], sum[N], f[N][N];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n; rep(i,1,n) cin >> a[i], sum[i] = sum[i - 1] + a[i], f[i][i] = 0;
rep(i,1,n) rep(j,i+1,n) f[i][j] = -0x3f3f3f3f;
rep(len,2,n) {
if (len % 3) {
for (int l = 1, r = l + len - 1; r <= n; ++ l, ++ r) {
rep(k,l,r - 1) f[l][r] = max(f[l][r], f[l][k] + f[k + 1][r]);
}
} else {
for (int l = 1, r = l + len - 1; r <= n; ++ l, ++ r) {
rep(k,l,r - 1) f[l][r] = max(f[l][r], f[l][k] + f[k + 1][r]);
f[l][r] = max(f[l][r], sum[r] - sum[l - 1]);
for (int k = l + 1; k <= r - 1; k += 3)
f[l][r] = max(f[l][r], f[l + 1][k - 1] + f[k + 1][r - 1] + a[l] + a[k] + a[r]);
}
}
} cout << f[1][n] << '\n';
}
有一个由房间组成的序列,其向左右两端无限延伸。你和 Snuke 在玩下面的游戏:
- 裁判给出一个由
B
和S
组成的字符串 \(t\),名为回合决定串,并将它展示给两名玩家。- 首先,Snuke 站在其中一个房间里。
- 随后,对于每个 \(i = 1,2,\dots,|t|\),发生如下的事件:
- 如果 \(t\) 中第 \(i\) 个字母为
B
,则这是你的回合。你会选择一个其中没有障碍或 Snuke 的房间,在其中放置一个障碍。这之后,如果 Snuke 所在房间左右两侧的房间均放置有障碍,则你胜利,游戏结束。- 如果 \(t\) 中第 \(i\) 个字母为
S
,则这是 Snuke 的回合。他可以移动到与当前所在房间相邻且无障碍的房间,或什么都不做。- 如果 \(|t|\) 轮后游戏仍未结束,则 Snuke 胜利,游戏结束。
给定字符串 \(s\),其中仅含有
B
、S
与?
三种字符。设 \(s\) 中含有 \(q\) 个?
,有 \(2^q\) 种填法将 \(s\) 中的?
替换为B
或S
,得到一个回合决定串。假设两名玩家都按最优策略操作,在这 \(2^q\) 个串中,有多少种填法使得你能胜利呢?请输出答案模 \(998244353\) 的值。
\(1\le |s|\le 10^6\)。
记 \((L,R)\) 为 Snuke 可能面对的一个局面,其中 \(L\) 为 Snuke 向左可以走入的房间数,\(R\) 为 Snuke 向右可以走入的房间数。有 \((L,R)\) 等价于 \((R,L)\)。
容易发现你操作的最优策略是将 Snuke 的一侧堵死,令其在这一侧可以走入的房间数为 \(0\)。重新陈述游戏:
Snuke 的初始局面为 \((\infty, \infty)\)。在 Snuke 的回合,他可以将 \((L,R)\) 的局面转移为 \((L\pm 1, R\mp 1)\) 或不变。在你的回合,你会将 \((L,R)\) 的局面转移为 \((\min\{L,R\},0)\)。
可以发现,你进行的每次操作都会至少将 Snuke 的可达范围缩小至原来的二分之一。因此你只需要操作 \(O(\log |s|)\) 次就可以胜利。
这个条件不好计数你胜利的串,但便于计数 Snuke 胜利的串。考虑首先求得在这 \(2^q\) 个串中 Snuke 胜利的串的数量,再用 \(2^q\) 减去其就是答案。
考虑倒序枚举。我们发现,当位置 \(i\) 的字符为 B
且其为倒数第 \(k\) 个时,必须保证 \(i\) 到下一个 B
之间存在至少 \(2^{k-1}\) 个 S
。只有这样才能使得 Snuke 在当前情况下不败。
这引出了 dp 的解法。
由于 dp 方程的第二维取值范围为 \(O(\log n)\),总时间复杂度 \(O(n\log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
#define rep(i,s,t) for (register int i = (s), i##_ = (t) + 1; i < i##_; ++ i)
const int N = 1e6 + 10, mod = 998244353;
int add(int a, int b) { return (a += b) >= mod ? a - mod : a; }
int n, ans = 1, lgv, f[N][24], cnt[N], len;
char s[N];
signed main() {
ios::sync_with_stdio(false); cin.tie(0), cout.tie(0);
cin >> s + 1; n = strlen(s + 1), lgv = __lg(n) + 1;
reverse(s + 1, s + 1 + n);
rep(i,1,n) {
if (s[i] == 'B') cnt[i + 1] = cnt[i] + 1;
else cnt[i + 1] = cnt[i];
if (s[i] == '?') ans = add(ans, ans);
} f[0][0] = 1;
rep(i,1,n) {
if (s[i] == 'S' or s[i] == '?') {
memcpy(f[i], f[i - 1], sizeof f[i]);
}
if (s[i] == 'B' or s[i] == '?') {
rep(j,1,lgv) {
len = 0; if (j > 1) len = 1 << j - 2;
int from = max(0, i - len - 1); len = min(len, i);
if (len == 0 or cnt[i] == cnt[i - len])
f[i][j] = add(f[i][j], f[from][j - 1]);
}
}
}
rep(i,0,lgv) ans = add(ans, mod - f[n][i]);
cout << ans << '\n';
}
有 \(N\) 个人编号从 \(1\) 到 \(N\), \(K\)个商品编号从\(1\)到\(K\)。从现在开始进行回合制的游戏。从号码为\(1\)的人开始,到号码为\(2\)的人,再到号码为\(3\)的人,号码为\(N\)的人,号码为\(1\)的人,\(\ldots\)号码为\(N\)的人,号码为\(1\)的人,\(\ldots\),他们将不断重复这一过程,直到所有商品被获得为止。
每个回合对应的人会进行以下的操作
自己已经获得商品的情况下,什么都不进行。
如果不是,这个人就从自己还没有选择的商品中,以等概率随机选择一个,秘密地告诉身为裁判的空井君。如果那个商品已经被别人获得了,就什么都不会发生。如果不是,那个商品就由那个人获得。
对于每个\(i\),请用\(\bmod \ 998244353\)来计算编号为\(i\)的人获得任一商品的概率(参见样例解释)。
\(1<=N,K<=40\)
这谁的翻译……反正我是不想改了
请自行去AtC看正常的英文吧
发现 \(n,k\le 40\)。这启发我们直接暴力。
我们设 \(f(i,p,fr,bk)\) 表示当前已经进行了 \(i\) 轮,当前进行到了没有获得商品的人中的第 \(p\) 个,需要求答案的是第 \(fr+1\) 个人,当前总共剩 \(fr + bk + 1\) 个人。
然后转移。
对于特定的 \(f(i,p,fr,bk)\),可以知道当前情况下剩余人数 \(R = fr + bk + 1\),当前情况下能拿到商品的概率 \(P = \frac{k - (n - C)}{k -i}\)。选完当前这个人后这个人再被选就是 \(\left\lfloor\frac{p}{len}\right\rfloor\) 局之后了,记这个玩意为 \(c\)。然后开始分讨:
- 当前选的人就是需要求的答案的人,即 \(p = fr + 1\):
赢了就是赢了,没拿到就大侠请重新来过
\(f(i,p,fr,bk) = P + (1 - P) \times f(i +c, p \bmod R + 1, fr, bk)\) - 当前选的人位于需要求答案的人的前面
赢了前面少一个,没拿到没变化
\(f(i,p,fr,bk) = P\times f(i +c, (p - 1) \bmod (R - 1) + 1, fr - 1, bk) + (1 - P) \times f(i +c, p \bmod R + 1, fr, bk)\) - 当前选的人位于需要求答案的人的后面
和 \(2.\) 一样。
写成记搜会好点。注意边界问题。
总状态数 \(O(n^4)\)。因此总时间复杂度 \(O(n^4)\)。
code
#include <bits/stdc++.h>
using namespace std;
template <typename T> void get(T & x) {
x = 0; char ch = getchar(); bool f = false; while (ch < '0' or ch > '9') f = f or ch == '-', ch = getchar();
while ('0' <= ch and ch <= '9') x = (x << 1) + (x << 3) + ch - '0', ch = getchar(); f && (x = -x);
} template <typename T, typename ... Args> void get(T & a, Args & ... b) { get(a); get(b...); }
#define rep(i,s,t) for (register int i = (s), i##_ = (t) + 1; i < i##_; ++ i)
const int N = 1e6 + 10;
int fac[N], inv[N], ifac[N];
const int mod = 998244353, g = 3;
const int inv2 = (mod + 1) >> 1;
template <typename T1, typename T2> T1 add(T1 a, T2 b) { return (a += b) >= mod ? a - mod : a; }
template <typename T1, typename ...Args> T1 add(T1 a, Args ... b) { return add(a, add(b...)); }
struct FastMod { int m; ll b; void init(int _m) { m = _m; if (m == 0) m = 1; b = ((lll)1<<64) / m; } FastMod(int _m) { init(_m); } int operator() (ll a) {ll q = ((lll)a * b) >> 64; a -= q * m; if (a >= m) a -= m; return a; } } Mod(mod);
int mul(int a, int b) { return Mod(1ll * a * b); } template <typename ...Args> int mul(int a, Args ...b) { return mul(a, mul(b...)); }
int n, k;
int _f[41][41][41][41];
int f(int i, int p, int fr, int bk) {
if (_f[i][p][fr][bk] != -1) return _f[i][p][fr][bk];
if (i == k or n - fr - bk - 1 == k) return _f[i][p][fr][bk] = 0;
int tot = fr + bk + 1, succ = mul(k - (n - tot), inv[k - i]), fail = add(1, mod - succ);
if (p == fr + 1) return _f[i][p][fr][bk] = add(succ, mul(fail, f(i + p / tot, p % tot + 1, fr, bk)));
else if (p <= fr) return _f[i][p][fr][bk] = add(mul(succ, f(i + p / tot, (p - 1) % (tot - 1) + 1, fr - 1, bk)), mul(fail, f(i + p / tot, p % tot + 1, fr, bk)));
else return _f[i][p][fr][bk] = add(mul(succ, f(i + p / tot, (p - 1) % (tot - 1) + 1, fr, bk - 1)), mul(fail, f(i + p / tot, p % tot + 1, fr, bk)));
}
signed main() {
get(n, k);
inv[0] = inv[1] = 1;
rep(i,2,n) inv[i] = mul(mod - mod / i, inv[mod % i]);
memset(_f, -1, sizeof _f);
rep(i,1,n) cout << f(0, 1, i - 1, n - i) << '\n';
}
于是 050 也就只剩下 E 和 F 了
F 是 4076 的金牌 我大概是没什么希望了
E …… 赛时无人 AC 因此在 kenkoooo 和 lg 上都没有难度评级
扫了一眼题解 似乎是个纯数论题