【NOI2024 D1T1】 集合 题解(可重集hash, 双指针)
自己做因为猜的结论并不与正解相似所以只能做到 \(80pts\)。 qwq
考察知识点:可重集 \(hash\),双指针。
分析:
首先来说一下我的思路以及一些部分分:
一. \(m \leq 5\)
这个是最简单的,并且分数不少。
考虑全排列枚举 \(p\),那么最多只有 \(m! = 120\) 种。
得到了映射方式我们只需要判断区间 \([l, r]\) 的每个集合是否相等即可。可以 \(O(1)\) 判断两个集合是否相等,把相等的位置看作 \(1\),不等的位置看作 \(0\)。那么在当前的映射方式下 \([l, r]\) 区间等价相当于 \([l, r]\) 的区间和等于 \(r - l + 1\)。前缀和可以 \(O(1)\) 判断。
因此每次询问枚举映射方式 \(O(1)\) \(check\) 即可。
复杂度 \(O(m!(n + q))\),期望得分 \(60pts\)。
namespace Part1 {
ull val[N * 3];
ull hs_a[121][N], hs_b[N], base = 131, B[N];
int rk, ta[N * 3];
bool vis[N * 3];
void get(int u) {
for(int i = 1; i <= n * 3; i ++ ) ta[i] = p[a[i]];
for(int i = 1; i <= n; i ++ ) {
ull tv = 0;
for(int j = 3 * (i - 1) + 1; j <= 3 * i; j ++ ) tv += val[ta[j]];
hs_a[u][i] = hs_a[u][i - 1] * base + tv;
}
}
void dfs(int x) {
if(x == m + 1) {
get(++ rk);
return ;
}
for(int i = 1; i <= m; i ++ ) {
if(!vis[i]) {
p[x] = i;
vis[i] = 1;
dfs(x + 1);
vis[i] = 0;
}
}
}
void solve() {
B[0] = 1; for(int i = 1; i <= n; i ++ ) B[i] = B[i - 1] * base;
for(int i = 1; i <= m; i ++ ) val[i] = rnd();
for(int i = 1; i <= n; i ++ ) {
ull tv = 0;
for(int j = 3 * (i - 1) + 1; j <= 3 * i; j ++ ) tv += val[b[j]];
hs_b[i] = hs_b[i - 1] * base + tv;
}
dfs(1);
for(int i = 1; i <= q; i ++ ) {
int l, r; scanf("%d%d", &l, &r);
bool f = 0;
for(int j = 1; j <= rk; j ++ ) {
if(hs_a[j][r] - hs_a[j][l - 1] * B[r - l + 1] == hs_b[r] - hs_b[l - 1] * B[r - l + 1]) f = 1;
}
if(f) puts("Yes");
else puts("No");
}
}
}
二.\(n \leq 2000\)
这时候 \(m\) 很大,显然不能全排列枚举了。我们需要思考 两个区间等价的充要条件:
我首先猜了一个结论:两个区间等价当且仅当任意一个子区间的数字种类数相等。
这个显然不正确,有以下反例:
{1, 2, 3}, {2, 3, 4}, {1, 2, 3}
{5, 6, 7}, {6, 7, 8}, {5, 7, 8}
发现此时 \(\{2, 3\}\) 肯定对应了 \(\{6, 7\}\),但是 \(3\) 位置上出现了 \(\{2, 3\}\) 下面却没出现 \(\{6\}\)。那么发现此时 \(6\) 的数量是较少的。观察发现上面有 \(3\) 个 \(2\), \(3\) 个 \(3\),下面却只有 \(7\) 是 \(3\) 个。
那么接着猜结论:两个区间等价的条件是 任意一个子区间的每种数字的出现次数的集合相等。
然后写了个暴力发现它是对的,具体证明我也不会。
然后优化到 \(O(n^2)\) 是比较容易的:
设 \(f_{l}\) 表示最大的 \(r\) 满足 区间 \([l, r]\) 是好的,那么不难证明 \([l, [l, r]]\) 都是好的。并且也可以发现 \(f_{l}\) 单调不降。因此可以双指针。
对于当前的 \(l\) 而言,考虑 \(check(r)\),那么只需要把新增的子区间 \([[l, r], r]\) 检验一遍就好了。
每次检验相当于比较两个可重集是否相等。做法有两种:
- 对每种出现次数 \(i\) 随机一个大权值 \(v_i\),然后将每种数 \(x\) 的出现次数 \(c_x\) 的 \(v_{c_x}\) 加起来,比较两个和是否相等。这种称为 和哈希。
- 对每种出现次数 \(i\) 随机一个大权值 \(v_i\),然后将所有 \(v_{c_x}\) 异或起来,比较两个异或和是否相等。这种称为 异或哈希。
大权值相加可以用 \(unsigned \ long \ long\) 的自然溢出。
从 \(r\) 往 \(l\) 扫更新集合的 \(hash\) 值复杂度 \(O(1)\),总复杂度 \(O(n^2)\)。加上之前的部分分期望得分 \(80pts\)。
namespace Part3 {
const int N = 2100;
int f[N];
int ca[N * 3], cb[N * 3];
ull val[N * 3], hs_a[N][N], hs_b[N][N];
void get() {
for(int i = 1; i <= n * 3; i ++ ) val[i] = rnd();
for(int l = 1; l <= n; l ++ ) {
for(int r = l; r <= n; r ++ ) {
ull ta = hs_a[l][r - 1], tb = hs_b[l][r - 1];
for(int j = 3 * (r - 1) + 1; j <= 3 * r; j ++ ) {
ta = ta - val[ca[a[j]]] + val[++ ca[a[j]]];
tb = tb - val[cb[b[j]]] + val[++ cb[b[j]]];
}
hs_a[l][r] = ta; hs_b[l][r] = tb;
}
for(int j = 3 * (l - 1) + 1; j <= 3 * n; j ++ ) ca[a[j]] = 0, cb[b[j]] = 0;
}
}
bool check(int l, int r) {
for(int ll = r; ll >= l; ll -- ) {
if(hs_a[ll][r] != hs_b[ll][r]) return 0;
}
return 1;
}
void get_f() {
int r = 1;
for(int i = 1; i <= n; i ++ ) {
while(r <= n && check(i, r)) r ++;
f[i] = r - 1;
}
}
void solve() {
get(); get_f();
for(int i = 1; i <= q; i ++ ) {
int l, r; scanf("%d%d", &l, &r);
if(f[l] >= r) puts("Yes");
else puts("No");
}
}
}
三.正解
刚才那个过程确实不太好优化。题解里有人写的这种做法然后乱搞过了,但是正确性并没有保证。
接着去思考充要条件,实际上有一种更妙的想法:
区间 \([l, r]\) 是好的,当且仅当 每一种数字出现的位置集合的集合相等。
这个充分性是好证明的:
充分性:由于存在一种映射排列,因此在这种排列下对应的两个数字的位置集合相等,集合的集合也相等。
必要性:满足这个条件只需要将位置集合相等的数构造为映射关系即可。
那么就可以优化求 \(f_i\) 的过程了:
每次移动 \(r\) 指针,位置集合改变的数字只有三个,因此暴力修改即可。
那么现在的问题是:如何 \(hash\) 集合的集合?
有三种做法:
- 对于位置集合的 \(hash\) 用 和哈希,然后将每个位置集合的哈希值求异或值比对。
- 位置集合用 和哈希,然后 将一个大数稳定映射成另一个大数,再次使用 和哈希。开不下数组,如何将大数稳定映射成另一个大数呢?只需要写一个 伪随机 的函数即可。
- 位置集合用 和哈希,然后插入哈希表里判断每种大数的数量差是否为 \(0\)。
上述做法都可以过,第一种比较好写。复杂度 \(O(n)\)。 期望得分 \(100pts\)。
namespace Part4 { // 考虑一个区间是好的的充要条件:每种数字出现的集合的集合相等
int f[N];
ull val[N], hs_a[N * 3], hs_b[N * 3], va, vb;
void add(int x, int k) {
for(int i = 3 * (x - 1) + 1; i <= 3 * x; i ++ ) {
va ^= hs_a[a[i]]; vb ^= hs_b[b[i]];
hs_a[a[i]] += val[x] * k; hs_b[b[i]] += val[x] * k;
va ^= hs_a[a[i]]; vb ^= hs_b[b[i]];
}
}
bool check(int r) {
add(r, 1); bool f = (va == vb);
add(r, -1); return f;
}
void get_f() {
int r = 1;
for(int i = 1; i <= n; i ++ ) {
while(r <= n && check(r)) r ++;
f[i] = r - 1; add(i, -1);
}
}
void solve() {
get_f();
for(int i = 1; i <= q; i ++ ) {
int l, r; scanf("%d%d", &l, &r);
if(f[l] >= r) puts("Yes");
else puts("No");
}
}
}
总结:
这道题是 DAY1 的签到题,但是并没有非常简单。
不过由于其部分分很多,加上一些奇怪的结论和数据不强的原因,这道题的区分度不大。
做到 \(80,90\) 分都不会影响取得银牌,但是如果相冲金牌还是要尽量做到满分的。

浙公网安备 33010602011771号