1.22 CW 模拟赛 T2. Mashmokh and Reverse Operation
思路
容易想到把区间建成满二叉树, 太 \(\rm{trick}\) 了
考虑把翻转操作搞到树上去
最初的想法是显然的, 对于 \(q_i\) 的询问, 显然要把从 \(2 \sim q_i + 1\) 层的左右儿子全部翻转, \(1\) 层是叶子节点
具体的, 可以把对于 \(u\) 的大区间这样处理
- 翻转 \(ls\)
- 翻转 \(rs\)
- 把 \(ls, rs\) 交换
前两个显然是递归下去的, 我们只需要对于每个节点, 维护第三个操作对答案的影响
块内处理
块内相当于原来的贡献是 \(\sum [a_i>a_j,i\in left,j\in right]\)
之后变成了 \(\sum [a_i>a_j,i\in right,j\in left]\)
考虑这个东西可以在归并排序的时候 \(\mathcal{O} (L \log L)\) 计算, 其中 \(L = 2^n\)
颓了一上午继续学, 以后手机放音乐就可以忍住不玩手机
这几天就是正常学就行了, 不要太颓
手机肯定是放外面的, 然后就是该怎么学怎么学, 可以考虑挂个什么音乐之类的就不颓了
具体的, 我们考虑归并排序的实质, 就是对于一个序列先分成这样的一颗满二叉树, 然后合并
在这个过程中, 我们显然是可以顺带求出逆序对个数的
当然如果你没有意识到, 在满二叉树上进行一个权值线段树的合并也是可以的
具体的, 你注意到线段树合并的复杂度大概是小的那一棵树的复杂度, 容易发现最劣 \((\)即每次两棵树大小相当\()\) 都是 \(\log\) 级别的
这个好像是一个 \(\rm{trick}\)
解决完这个问题, 如何处理每次答案呢
显然暴力枚举或者仅和上面一样模拟都是纯神经, 所以考虑一个传统 \(\rm{trick}\) , 即每次操作对答案的影响
首先我们知道这些信息
- 二叉树中每个节点对应的区间的顺逆序对个数
- 每次操作需要对那些层进行翻转
考虑这个问题的子问题, 即每一层进行翻转产生的贡献
不难发现, 我们可以直接计算出每一层的顺逆序对交换之后的贡献, 块内部分轻松解决
块间处理
你使用脑子, 发现交换前后块间逆序对个数不受影响, 那直接不管这个就行了
最终处理
首先归并处理每个节点的顺逆序对个数
然后每次询问查询 \(2 \sim q_i\) 层的节点的顺逆序对之差 \((\)注意这个顺逆序对的定义是会变的\()\)
如果你提前统计每一层的顺逆序对之差, 那么诗人能做
实现
框架
首先归并两次求层内逆序对个数, 标记一下
然后处理的时候一次处理一层即可
代码
#include <bits/stdc++.h>
#define int long long
typedef unsigned long long ull;
const int MAXN = 2e6 + 20;
const int MAXLOGN = 25;
namespace Fast_IO {
char buf[1 << 20], *p1, *p2;
#define getchar() (p1 == p2 and (p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin), p1 == p2) ? 0 : *p1++)
void read() {}
template <class T, class ...T1>
void read(T &x, T1 &...y) {
x = 0;
char ch = getchar(); bool f = 1;
for (; ch < '0' or ch > '9'; ch = getchar()) if (ch == '-') f = 0;
for (; ch >= '0' and ch <= '9'; x = x * 10 + (ch & 15), ch = getchar());
x = (f ? x : -x);
read(y...);
}
void print(int x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) print(x / 10);
putchar(x % 10 + '0');
}
void print(int x, char c) { print(x), putchar(c); }
} using namespace Fast_IO;
int n, m;
int inver[2][MAXLOGN];
int val[MAXN], bin[MAXN], q[MAXN];
ull k1, k2, threshold;
ull xorShift128Plus() {
ull k3 = k1, k4 = k2;
k1 = k4;
k3 ^= (k3 << 23);
k2 = k3 ^ k4 ^ (k3 >> 17) ^ (k4 >> 26);
return k2 + k4;
}
void gen(int n, int m, int threshold, ull _k1, ull _k2) {
k1 = _k1, k2 =_k2;
for (int i = 1; i <= (1 << n); i++) bin[i] = val[i] = xorShift128Plus() % threshold + 1;
for (int i = 1; i <= m; i++) q[i] = xorShift128Plus() % (n + 1);
}
/*归并排序计算每个节点的逆序对个数*/
class mergesort
{
private:
public:
/*合并*/
int merge(const int *a, size_t asize, const int *b, size_t bsize, int *c) {
size_t i = 1, j = 1, k = 1;
int res = 0; // 逆序对个数
while (i <= asize && j <= bsize) if (b[j] < a[i]) res += asize - i + 1, c[k++] = b[j++]; else c[k++] = a[i++];
for (; i <= asize; i++) c[k++] = a[i];
for (; j <= bsize; j++) c[k++] = b[j];
return res;
}
int tmp[MAXN];
/*倍增法处理逆序对数量*/
void solve(int *val, size_t n, int type) {
memset(tmp, 0, sizeof tmp);
for (size_t seg = 1, res = 2; seg < n; seg <<= 1, res++) for (size_t left1 = 0; left1 < n - seg; left1 += seg + seg) {
size_t right1 = left1 + seg, left2 = right1, right2 = std::min(left2 + seg, n);
inver[type][res] += merge(val + left1, right1 - left1, val + left2, right2 - left2, tmp + left1);
for (size_t i = left1 + 1; i <= right2; i++) val[i] = tmp[i];
}
}
} ms;
signed main()
{
read(n, m, threshold, k1, k2);
gen(n, m, threshold, k1, k2);
int logn = n;
n = (1 << n);
ms.solve(bin, n, 0);
int sum = inver[0][logn];
std::reverse(val + 1, val + n + 1); for (int i = 1; i <= n; i++) bin[i] = val[i];
ms.solve(bin, n, 1);
int tot = 0;
for (int i = 1; i <= m; i++) {
for (int j = 2; j <= q[i] + 1; j++) std::swap(inver[0][j], inver[1][j]);
int ans = 0; for (int j = 1; j <= logn; j++) ans += inver[0][j + 1];
tot ^= (ans * i);
}
print(tot);
return 0;
}
总结
常用的树上 \(\rm{trick}\)
然后就是善于把这类问题转化成对答案的 \(\Delta\) 的处理, 会方便很多
对归并的理解是一坨, 赛时根本想不到
常用的优化办法: 把一定一起操作的绑定到一起
逆序对的性质:
如果用倍增法来计算逆序对个数, 本质上是将数列划分成一棵这样的满二叉树\((\)一般情况下可能不满\()\) , 然后任意一对逆序对只会在其 \(\rm{LCA}\) 处合并

浙公网安备 33010602011771号