Hello 2022
\(\inf\) 年没有打 CF 后被自己的手速感动。
Hello 2022,也算是对 2022 的期望叭。
HaHa, The best contest in Codeforces in 2022 so far
。
A - Stable Arrangement of Rooks
简单模拟。
B - Integers Shop
简单贪心。
C - Hidden Permutations
交互题,给定一个排列 \(p\),一开始 \(a_i=i\)。
每次可以询问某个位置的 \(a_i\),每次询问后 \(a\) 序列会轮换一次,方式为 \(a_{p_i}\to a_i\)。
利用不超过 \(2n\) 次询问确定 \(p\)。
不论何时,假设此次询问 \(a_i=x\),紧接着再问一次 \(a_i=y\),则 \(p_x=y\)。
对每个环统计答案,确定一个长度为 \(L\) 的环需要 \(L+1\) 次询问,故不超过 \(2n\)。
void Work() {
n = read();
rep(i, 1, n) ans[i] = 0;
int t;
rep(i, 1, n) if(! ans[i]) {
int s = Que(i), S = s;
t = 0;
a[++ t] = s;
while(true) {s = Que(i); if(s == S) break; a[++ t] = s;}
rep(j, 1, t - 1) ans[a[j]] = a[j + 1];
ans[a[t]] = a[1];
}
printf("! ");
rep(i, 1, n) printf("%d ", ans[i]); cout << endl;
}
D - The Winter Hike
在 \(2n\times 2n\) 的网格中,将左上角 \(n\times n\) 个人移动到右下角 \(n\times n\) 个格子,对应顺序随意。
经过一个格子就要付出它上面数字的代价,只统计一次。
移动方式为 一行 或 一列 的 左右 或 上下,出格就循环补充到另一端,求最小代价。
首先右下角的所有代价都一定加上。
- 可以证明只要左上到右下有一条路,就能够移动所有人。
但是直接最短路是不正确的,因为无论如何,有 \(8\) 个格子必须经过至少其一,它们是
\((1,n+1),(1,2n),(n,n+1),(n,2n),(n+1,1),(2n,1),(n+1,n),(2n,n)\)
同时,若它们其一被经过了,那就已经形成了通路,故答案就是上面八个的 min
。
E - New School
给定 \(n\) 个老师,年龄分别为 \(a_i\),和 \(m\) 个学生群体,每个群体中有 \(k_i\) 个学生,年龄分别为 \(b_{i,j}\)。
要求给老师学生配对,每个学生群体安排恰好一名老师,老师的年龄不小于该群体的平均年龄。
对于每个学生,询问若该学生退出所在群体,能否配对成功。
如果是单次询问,直接排序后贪心。
如果有学生退出,不难发现贪心操作匹配的老师至多 \(+1/-1\),故直接三个树状数组维护即可。
struct Tree {
int c[N];
void Init(int n) {rep(i, 0, n) c[i] = 0;}
void Add(int x, int v) {for(; x <= n; x += x & -x) c[x] += v;}
int Ask(int x) {int sum = 0; for(; x; x -= x & -x) sum += c[x]; return sum;}
int Que(int L, int R) {return ((R - L + 1) == (Ask(R) - Ask(L - 1)));}
} Mt, Lt, Rt;
void Clear() {Mt.Init(n), Lt.Init(n), Rt.Init(n); t = 0;}
bool cmp1(int x, int y) {return x > y;}
bool cmp2(Node x, Node y) {return x.s * y.k > y.s * x.k;}
bool Get(int p, int v) {
int l = 0, r = n;
LL nk = stu[p].k - 1, ns = stu[p].s - v;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(stu[mid].s * nk >= ns * stu[mid].k) l = mid;
else r = mid - 1;
}
int q = l;
if(p <= q) {
if(1 <= p - 1 && ! Mt.Que(1, p - 1)) return false;
if(q + 1 <= n && ! Mt.Que(q + 1, n)) return false;
if(p + 1 <= q && ! Lt.Que(p + 1, q)) return false;
if(ns > 1LL * a[q] * nk) return false;
return true;
}
else {
if(1 <= q && ! Mt.Que(1, q)) return false;
if(p + 1 <= n && ! Mt.Que(p + 1, n)) return false;
if(q + 1 <= p - 1 && ! Rt.Que(q + 1, p - 1)) return false;
if(ns > 1LL * a[q + 1] * nk) return false;
return true;
}
}
void Work() {
m = read(), n = read();
Clear();
rep(i, 1, m) a[i] = read();
sort(a + 1, a + m + 1, cmp1);
rep(i, 1, n) {
stu[i].k = read();
stu[i].id = i;
stu[i].s = 0;
rep(j, 1, stu[i].k) {
t ++;
pos[t] = i;
age[t] = read();
stu[i].s += age[t];
}
}
sort(stu + 1, stu + n + 1, cmp2);
rep(i, 1, n) to[stu[i].id] = i;
rep(i, 1, n) if(stu[i].s <= 1LL * a[i] * stu[i].k) Mt.Add(i, 1);
rep(i, 2, n) if(stu[i].s <= 1LL * a[i - 1] * stu[i].k) Lt.Add(i, 1);
rep(i, 2, n) if(stu[i - 1].s <= 1LL * a[i] * stu[i - 1].k) Rt.Add(i - 1, 1);
stu[0].k = 0, stu[0].s = 1e10;
rep(i, 1, t) ans[i] = Get(to[pos[i]], age[i]);
rep(i, 1, t) putchar(ans[i] ? '1' : '0');
puts("");
}
F - Strange Instructions
给定 01 串,有三种操作:
- 将一个
00
子串变为0
,得到 \(a\) 的价值。- 将一个
11
子串变为1
,得到 \(b\) 的价值。- 将一个
0
删去,扣除 \(c\) 的价值。要求操作奇偶交替进行,最大化最终价值。
直觉上很可以贪心。
不难发现 1
的操作比较平凡,只需要统计一个数值 \(one\) 表示能进行操作 \(2\) 的次数即可完全代替它。
一般情况,显然选择 \(1,2\) 操作交替进行最优。但是当不能进行其中之一时,可能需要通过 \(3\) “续命”。
本着这些理念,稍加思考得到一个完整的贪心体系:
- 需要进行操作 \(2\)
- 若还能进行,任选一个子串变换。
- 否则结束。
- 需要进行操作 \(1/3\)
- 若操作 \(2\) 无法再进行
- 若能够进行操作 \(1\),尝试进行它(并非真的进行),这是这种情况的最后一次操作。
- 若能够进行操作 \(3\) 消除形如
101
中的0
, 进行它。 - 否则结束。
- 若操作 \(2\) 还能进行
- 若能在非边角块进行操作 \(1\),任选一个最短的块操作它。
- 否则,若能在边角块进行操作 \(1\),操作它。(A)
- 否则,若能在非边角块进行操作 \(3\),任选一个最短的块操作它。(B)
- 否则,若能在边角块进行操作 \(3\),操作它。
- 否则结束。
- 若操作 \(2\) 无法再进行
哈,这就真的结束了。
唯一值得注意的是 A B 的优先顺序,因为当前操作 \(2\) 还能进行,故不一定需要续命,先进行 \(1\) 不劣。
int n, a, b, c, arc[N], len[N];
LL Get(int oo) {
int one = 0, cor1 = 0, cor2 = 0, mid = 0;
int now = 1;
bool flag = false;
rep(i, 1, n) arc[i] = 0;
rep(i, 2, n + 1)
if(str[i] == str[i - 1]) now ++;
else {
int o = str[i - 1] - '0';
if(o) one += now - 1;
else {
if(! flag && i != n + 1) cor1 = now;
if(i == n + 1) cor2 = now;
if(flag && i != n + 1) {
now --;
if(now) arc[now] ++;
else mid ++;
}
}
flag = true, now = 1;
}
int t = 0;
per(i, n, 1) while(arc[i] --) len[++ t] = i;
LL ans = 0, Ans = 0;
while(true) {
if(oo) {
if(one)
ans += b, one --, Ans = max(Ans, ans);
else
break;
}
else {
if(! one) {
if((t && len[t]) || cor1 > 1 || cor2 > 1) Ans = max(Ans, ans + a);
if(mid) mid --, one ++, ans -= c;
else break;
}
else {
if(t && len[t]) {
ans += a, Ans = max(Ans, ans);
if(! -- len[t]) mid ++, t --;
}
else if(cor1 > 1 || cor2 > 1) {
if(cor1 > 1) cor1 --; else cor2 --;
ans += a, Ans = max(Ans, ans);
}
else if(mid) {
ans -= c, mid --, one ++;
}
else if(cor1 || cor2) {
if(cor1) cor1 --; else cor2 --;
ans -= c;
}
else break;
}
}
oo ^= 1;
}
return Ans;
}
void Work() {
n = read(), a = read(), b = read(), c = read();
scanf("%s", str + 1);
str[n + 1] = '#';
printf("%lld\n", max(Get(0), Get(1)));
}
G - Weighted Increasing Subsequences
统计所有严格上升子序列的价值和。
对于子序列 \(a_{k_1}<a_{k_2}<\cdots<a_{k_m},k_1<k_2<\cdots<k_m\),它的价值为:
\[\sum\limits_{i=1}^m [a_{k_i}<\max(a_{k_m+1}\sim a_n)]$$。 \]
首先离散化到一个排列,方法是按照 \((a_i,-i)\) 排序,这是一个应对子序列问题的常见 trick。
对于每个位置单独统计,即统计经过这个位置且它对价值有贡献的子序列数。
预处理 \(pre_i,suf_i\) 表示从 \(i\) 开始或结束的上升子序列数,这个是经典的 DP 问题,可以用树状数组优化。
考虑统计从 \(i\) 开始,但是最终后面没有数 \(>a_i\) 的方案数 \(bac_i\),那么 \(ans=\sum pre\times(suf-bac)\)。
观察发现,最后一个比 \(a_i\) 大的数一定是一个后缀 Max,所以可以直接处理出后缀 Max 序列出来。
设为 \(p_1>p_2>...(a_{p_1}<a_{p_2}<...)\),对每个位置找到最小的 \(q\) 满足 \(a_{p_q}>a_i\)。
那么只需要统计从 \(a_i\) 出发,到 \(a_{p_q}\) 结束的子序列数即可(一定是恰好于 \(p_q\) 结束,往后的话不满足 \(q\) 的最小性)
这个问题可以用相似的树状数组优化 DP 解决。
保证复杂度的还有一个重要性质,对于 \(q\),只需要针对值域为 \((a_{p_{q-1}},a_{p_q}]\) 的位置 DP,同样是 \(q\) 的最小性。
最终复杂度 \(O(n\log n)\)。
const int N = 2e5 + 10;
const LL P = 1e9 + 7;
int n, t, a[N], pos[N], Mx[N], val[N];
int cnt, head[N], nxt[N], ver[N];
LL pre[N], suf[N], bac[N], num[N << 1];
pair<int, int> arc[N], p[N << 1];
struct BIT {
LL c[N];
void Init(int n) {rep(i, 1, n) c[i] = 0;}
void Add(int x, LL v) {for(; x <= n; x += x & -x) c[x] = (c[x] + v) % P;}
LL Ask(int x) {LL sum = 0; for(; x; x -= x & -x) sum = (sum + c[x]) % P; return sum;}
LL Que(int L, int R) {LL now = Ask(R) - Ask(L - 1); return (now % P + P) % P;}
} At;
void Clear() {
cnt = t = 0;
rep(i, 1, n)
pre[i] = suf[i] = bac[i] = num[i] = 0,
head[i] = nxt[i] = ver[i] = pos[i] = Mx[i] = val[i] = 0;
}
void Ins(int u, int v) {nxt[++ cnt] = head[u], ver[cnt] = v, head[u] = cnt;}
void Work() {
n = read(); Clear();
rep(i, 1, n) a[i] = read(), arc[i] = make_pair(a[i], -i);
sort(arc + 1, arc + n + 1);
rep(i, 1, n) a[pos[i] = - arc[i].second] = i;
At.Init(n);
rep(i, 1, n) pre[i] = (At.Que(1, a[i]) + 1) % P, At.Add(a[i], pre[i]);
At.Init(n);
per(i, n, 1) suf[i] = (At.Que(a[i], n) + 1) % P, At.Add(a[i], suf[i]);
Mx[0] = -1;
per(i, n, 1)
if(a[i] > val[t]) Mx[++ t] = i, val[t] = a[i], bac[i] = 1;
else {
int o = lower_bound(val + 1, val + t + 1, a[i]) - val;
Ins(o, i);
}
At.Init(n);
a[Mx[0] = 0] = 0;
rep(i, 1, t) if(head[i]) {
int L = a[Mx[i - 1]] + 1, R = a[Mx[i]], tot = R - L + 1;
rep(j, L, R) p[j - L + 1] = make_pair(pos[j], 0);
for(int j = head[i]; j; j = nxt[j]) {
int now = ver[j];
p[++ tot] = make_pair(now, 1);
}
sort(p + 1, p + tot + 1);
while(p[tot].first != Mx[i]) tot --;
At.Add(a[p[tot].first], 1);
per(j, tot - 1, 1) {
int o = p[j].first;
if(p[j].second)
bac[o] = At.Que(a[o], n);
else {
num[j] = At.Que(a[o], n);
At.Add(a[o], num[j]);
}
}
At.Add(a[p[tot].first], -1);
per(j, tot - 1, 1)
if(! p[j].second) At.Add(a[p[j].first], - num[j]), num[j] = 0;
}
LL ans = 0;
rep(i, 1, n) {
LL A = pre[i];
LL B = ((suf[i] - bac[i]) % P + P) % P;
ans = (ans + A * B % P) % P;
}
printf("%lld\n", ans);
}