2024.10.21 杂题
2024.10.21 杂题
P11217 【MX-S4-T1】「yyOI R2」youyou 的垃圾桶
\(O(n \log n)\) 线段树二分不会,想写 \(O(q \log ^ 2n)\) 的二分,但是 htdlz 说常数大可能过不去。所以我选择写树状数组实现的 \(O(q \log^2 n)\) 做法然后跑的飞快比线段树二分还快直接过了(doge)
记录前缀和 \(s[i]\),由于我们写的树状数组没有建树操作。所以对于每一次 \(add(l,r,k)\) 我们的实际 \(\sum a_i\) 是 \(s[n]+ask(n)\)。用 \(cnt\) 记录我们能被桶踹多少轮,由于每一波伤害翻倍。所以 \(cnt\) 为 \(\log_{2}{\frac{w}{s[n]+ask(n)}}\)。再记录被踹到最后一轮剩了多少血。如果刚好被踹死,答案就是 \(cnt \times n - 1\)。如果还有一点血,就二分计算在下标为多少的时候被踹死。二分是 \(\log n\) 的,但是每次二分都要调用 \(ask\),\(ask\) 也是 \(\log n\) 的,所以总复杂度 \(O(n \log ^ 2n)\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 5;
int n, q, w;
int a[N];
int s[N];
struct BIT
{
int c[2][N];
int lowbit(int x){return x & -x;}
void _add(int k, int x, int y)
{
for (; x <= n; x += lowbit(x))
c[k][x] += y;
}
int _ask(int k, int x)
{
int res = 0;
for (; x; x -= lowbit(x))
res += c[k][x];
return res;
}
void add(int l, int r, int k)
{
_add(0, l, k), _add(0, r + 1, -k);
_add(1, l, l * k), _add(1, r + 1, -(r + 1) * k);
}
int ask(int x)
{
return (x + 1) * _ask(0, x) - _ask(1, x);
}
} tree;
int calc(int x, int t)
{
int l = 1, r = n;
while (l < r)
{
int mid = (l + r) >> 1;
if ((s[mid] + tree.ask(mid)) * t >= x) r = mid;
else l = mid + 1;
}
return l;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> q >> w;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
while (q--)
{
int l, r, k;
cin >> l >> r >> k;
tree.add(l, r, k);
int p = s[n] + tree.ask(n);
// 因为原来的答案并没有加入到树状数组里 所以为 s[n]+ask(n)
int cnt = 0;//记录能打多少轮
cnt = log2(w / p + 1);
int ck = w - ((1ll << cnt) - 1) * p;//看看打完后剩多少血
if (!ck) //刚好死掉
{
cout << cnt * n - 1 << endl;
continue;
}
int ans1 = calc(ck, 1ll << cnt) - 1;//二分答案计算最后一次在哪个位置死掉
cout << cnt * n + ans1 << endl;
}
return 0;
}
P11218 【MX-S4-T2】「yyOI R2」youyou 不喜欢夏天
观察数据范围,复杂度瓶颈不能超过 \(n \log n\)。想带个 \(\log\) 又不能排序又不能数据结构维护。那正解应该是 \(O(n)\) 的。考虑贪心,只能观察出一些性质。
以一列为单位,先不考虑是不是联通块,对于两黑或两白是简单的,两黑一起选了一定更优,两白一定尽可能不选。对于一黑一白有两种玩法,第一种是如果 \(m\) 特别小 \(n\) 特别大,那么尽可能多的选一黑一白中的黑,不选白。因为多选一个黑就能赚一点,\(m\) 很小 yy 不能怎么改变结局。或者一黑一白一起选了 yy 就算翻转了对答案也没影响。
问题在于,什么时候选择一黑一白策略?什么时候选择一黑策略?怎么判断是不是联通块?贪心显然是不行了。由于是一列一列扫的,每一列都有很强的关联性。并且对于联通块左边界 \(i\),从 \(i\) 开始扫,如果答案不优秀了要把联通块断开重新算,dp 可以维护这个。所以考虑进行 dp。
\(f_i\) 表示在选择一黑一白的时候两个一块办了的答案,\(g_i\) 表示在选择一黑一白的时候只选择一个黑的答案。数组 w[i] = (c[i] - '0') * 2 + (d[i] - '0')
以此来记录当前列的形式。
如果 \(w_i=3\),那么 \(f_i=f_{i-1}+2\),\(g\) 的转移一样。
如果 \(w_i = 0\),那么只选一个白的作为过渡或者直接断开重新开始,\(f_i = max\{f[i - 1] - 1, 0\}\),\(g[i]\) 同理
如果 \(w_i=1/2\),那么 \(f_i=f_{i-1}\),因为选上这一列对答案没影响。而对于 \(g\) 考虑的就多了,特判如果出现了两个白隔断不能进行只选一个的操作 ,以及两种不同的一黑一白交替出现了导致只选黑色不能连起来,没有这些这些情况 \(g_i = g_{i - 1} + 1\),否则 \(g_i=g_{i-1}\)。开个 \(lst\) 辅助判断情况。
对于 \(f_i\) ,最后的答案就是 \(f_i\),而对于 \(g_i\),由于有很多一黑一白只选一个黑的情况,yy 可以通过翻转操作让他变成白从而让答案减小 \(2\),所以 \(g_i\) 最后的答案要减去一个 \(2\times m\)。
对于最终的答案就是 \(\max(\max_{i=1}^{n}(g_i-2\times m),\max_{i=1}^{n}f_i)\)
诶,问题来了,我们的 \(g\) 转移其实是有问题的,因为我们可能中间选择的一黑一白选一黑的情况并不够 \(m\) 次导致最后计算出来的答案偏小。没关系哒,因为我们最终的答案是对多个答案取最大值,而我们的考虑不周只是会导致部分 \(g_i\) 算的比较小,但是不会影响最终结果。
复杂度 \(O(n)\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
#define m(a) memset(a, 0, sizeof a)
using namespace std;
const int N = 2e6 + 5;
const int inf = 1e9;
int C, T, n, m;
int a[N], b[N];
int w[N];
int f[N], g[N];
/*
f[i] 表示在选择一黑一白的时候两个一块办了的答案
g[i] 表示在选择一黑一白的时候只选择一个黑的答案
*/
char c[N], d[N];
signed main()
{
cin >> C >> T;
while (T--)
{
cin >> n >> m;
int lst = 0, ans = 0;
scanf("%s%s", c + 1, d + 1);
for (rint i = 1; i <= n; i++) w[i] = (c[i] - '0') * 2 + (d[i] - '0');
m(g), m(f);
for (rint i = 1; i <= n; i++)
{
if (w[i] == 3)
{
f[i] = f[i - 1] + 2;
g[i] = g[i - 1] + 2;
lst = 3;
}
else if (w[i] == 0)
{
f[i] = max(f[i - 1] - 1, 0ll);
if (g[i - 1] > 1) g[i] = g[i - 1] - 1;
else g[i] = 0, lst = 3;
}
else
{
f[i] = f[i - 1];
//这个时候为一黑一白 f[]两个一起选了 答案不变
if ((lst + w[i]) != 3)
/*
特判如果出现了两个白隔断不能进行只选一个的操作
以及两种不同的一黑一白交替出现了导致只选黑色不能连起来
没有这些这些情况 g[i] = g[i - 1] + 1
*/
{
g[i] = g[i - 1] + 1;
lst = w[i];
}
else
{
g[i] = g[i - 1];
lst = 3;
}
}
ans = max({ans, g[i] - 2 * m, f[i]});
}
cout << ans << endl;
}
return 0;
}
P11219 【MX-S4-T3】「yyOI R2」youyou 的序列 II
感谢 ReTF 提供的帮助
首先,如果询问的区间中含有大于 \(w_1\) 的数字,那么 youyou 必然失败。
称长度为 \(c_2\),总和大小大于 \(w_2\) 的子区间为合法区间,也即 yy 可以操作的区间。
- 性质 1:对于不存在任何一个“合法区间”的序列,youyou 显然必胜
- 性质 2:存在“合法区间”,若 youyou 可以一次性染红所有未染红的合法区间,则 youyou 必胜
- 性质 3:存在“合法区间”,若 youyou 不可以做到一次性染红所有未染红的合法区间,则 yy 必胜
对于 yy 而言的最优策略是:尽量在整个序列的边缘进行染色。yy 的目的是防止 youyou 把整个序列染成红色。设所有“合法区间”中位于最左边的左端点为 \(l\),最右边的右端点为 \(r\)。如果满足性质 3,只考虑位置 \(l,r\) ,若 youyou 每次把哪个点染了,yy 就可以跟着染,然后陷入循环,youyou 必败。所以只要 youyou 无法一次染红 \(l\) 和 \(r\) ,则 yy 必胜。
直接维护原序列使用线段树即可,非常简单。实现瓶颈在于如何求出上述的 \(l,r\)
粉兔讲的是线段树二分,每个叶子记录一个长度为 \(c_2\) 的区间,将单点修改转为区间修改。
这里采用的实现方式是使用势能线段树,每个点维护的是 [i,i + d - 1]
的元素和。那么当当前点势能为 \(0\) 时并且 \(l=r\),那么将 [i,i + d - 1]
存入即可。存储可以开个 set
。由于每次 change
只会在 set
中 insert
一次,所以复杂度为 \(O(n \log n)\)
补充:关于势能线段树
之前并没有整理过相关笔记。马上退赛了也必要专门拿出来时间做笔记,大概记录一下原理。很多区间修改操作是不能依靠懒标记完成,因为很多运算都是依赖于叶子节点的值的一直递归到叶子结点一个一个改显然无法接受。每一个操作,总会使得其能够接受的继续进行修改的次数越来越少。比如一开始位于高空,每次修改使高度下降势能变小,当势能为 \(0\) 时再去对接来下的操作就没有意义了可以直接停了。
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
#define ls p << 1
#define rs p << 1 | 1
using namespace std;
const int N = 3e5 + 5;
const int M = 1 << 20;
const int inf = 1e18;
int n, q, c1, c2, w1, w2;
int a[N];
set<int> L, R;
int s[N];
struct SegmentTree1
{
struct node
{
int v, sum;
} t[M];
void push_up(int p)
{
t[p].v = max(t[ls].v, t[rs].v);
t[p].sum = t[ls].sum + t[rs].sum;
}
void build(int p, int l, int r)
{
if (l == r)
{
t[p].v = t[p].sum = a[l];
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
push_up(p);
}
void change(int p, int l, int r, int x, int d)
{
if (l == r)
{
t[p].v = t[p].sum = d;
return;
}
int mid = (l + r) >> 1;
if (x <= mid) change(ls, l, mid, x, d);
else change(rs, mid + 1, r, x, d);
push_up(p);
}
int query_sum(int p, int l, int r, int x, int y)
{
if (x <= l && r <= y) return t[p].sum;
int mid = (l + r) >> 1;
int res = 0;
if (x <= mid) res += query_sum(ls, l, mid, x, y);
if (y > mid) res += query_sum(rs, mid + 1, r, x, y);
return res;
}
int query_max(int p, int l, int r, int x, int y)
{
if (x <= l && r <= y) return t[p].v;
int mid = (l + r) >> 1;
if (x <= mid) return query_max(ls, l, mid, x, y);
if (y > mid) return query_max(rs, mid + 1, r, x, y);
return max(query_max(ls, l, mid, x, y), query_max(rs, mid + 1, r, x, y));
}
} tree1;
struct SegmentTree2
{
struct node
{
int minn, lazy;
} t[M];
void push_up(int p)
{
t[p].minn = min(t[ls].minn, t[rs].minn);
}
void push_down(int p)
{
t[ls].minn -= t[p].lazy, t[rs].minn -= t[p].lazy;
t[ls].lazy += t[p].lazy, t[rs].lazy += t[p].lazy;
t[p].lazy = 0;
}
void build(int p, int l, int r)
{
t[p].minn = inf;
if (l == r) return ;
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
push_up(p);
}
void pos_change(int p, int l, int r, int x, int y)
{
if (l == r)
{
t[p].minn = min(t[p].minn, y);
return;
}
int mid = (l + r) >> 1;
if (x <= mid) pos_change(ls, l, mid, x, y);
else pos_change(rs, mid + 1, r, x, y);
push_up(p);
}
void range_change(int p, int l, int r, int x, int y, int d)
{
if (t[p].minn >= inf) return;
if (x <= l && r <= y)
{
if (t[p].minn > d)
{
t[p].minn -= d;
t[p].lazy += d;
}
else if (l == r)
{
t[p].minn = inf;
L.insert(l);
R.insert(l + c2 - 1);
}
else
{
push_down(p);
int mid = (l + r) >> 1;
range_change(ls, l, mid, x, y, d);
range_change(rs, mid + 1, r, x, y, d);
push_up(p);
}
return ;
}
push_down(p);
int mid = (l + r) >> 1;
if (x <= mid) range_change(ls, l, mid, x, y, d);
if (y > mid) range_change(rs, mid + 1, r, x, y, d);
push_up(p);
}
} tree2;
int findL(int x) {return *L.lower_bound(x);}
int findR(int x) {return *--R.upper_bound(x);}
signed main()
{
cin >> n >> q >> c1 >> c2 >> w1 >> w2;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
s[i] += s[i - 1] + a[i];
}
if (c2 > n) c2 = n;
tree1.build(1, 1, n);
tree2.build(1, 1, n - c2 + 1);
L.insert(0), L.insert(n + 1);
R.insert(0), R.insert(n + 1);
for (rint i = 1; i <= n - c2 + 1; i++) //枚举左端点
{
int sum = s[i + c2 - 1] - s[i - 1];//区间和
if (sum <= w2) tree2.pos_change(1, 1, n - c2 + 1, i, w2 - sum + 1);
//tree2 是一颗势能线段树
//每个点维护的是 [i,i + d - 1] 的元素和
else L.insert(i), R.insert(i + c2 - 1);//大于 w2 全选更优
}
while (q--)
{
int opt;
cin >> opt;
if (opt == 1)
{
int x, y;
cin >> x >> y;
a[x] += y;
tree1.change(1, 1, n, x, a[x]);
if (max(x - c2 + 1, 1ll) <= min(x, n - c2 + 1))
tree2.range_change(1, 1, n - c2 + 1, max(x - c2 + 1, 1ll), min(x, n - c2 + 1), y);
//单点修改同时在tree2更新存储 l, r
}
else
{
/*
称长度为 c2,总和大小大于 w2 的子区间为合法区间 也即 yy 可以操作的区间
*/
int l, r;
cin >> l >> r;
if (tree1.query_max(1, 1, n, l, r) > w1) puts("tetris");
// youyou 没法出手直接输掉
else
{
if (r - l + 1 <= c2)
{
if (tree1.query_sum(1, 1, n, l, r) > w2)
// 如果 yy 能一次性全染上
{
if (r - l + 1 <= c1 && tree1.query_sum(1, 1, n, l, r) <= w1) puts("cont");
// 如果 youyou 能一次全染上 则 youyou 一定能赢
else puts("tetris");
// 否则 yy 一直一次性全染上 youyou 赢不了
}
else puts("cont");
// yy 不能一次性全部解决掉,则 youyou 一定赢
}
else
{
int L = -1, R = -1;
L = findL(l), R = findR(r);
// 查找当前情况左右端点
if (L == -1 || R == -1 || L > R || R - L + 1 < c2) puts("cont");
// 如果 L,R 没找到或者 L 比 R 大又或者区间长度小于 c2
// 即不存在合法区间
// 那么 youyou 一定赢
else if (R - L + 1 <= c1 && tree1.query_sum(1, 1, n, L, R) <= w1) puts("cont");
// 同样,如果 youyou 能一次全染上 则 youyou 一定能赢
else puts("tetris");
// 否则 yy 赢
}
}
}
}
return 0;
}