HDOJ 6964 I love counting
题目链接
题目大意
给定一个长度为 \(n\) 的序列 \(\{c\}\),\(q\) 次询问,每次给出 \(l,r,a,b\),求在 \([l,r]\) 中有多少种不同的值 \(k\) 满足 \(k\oplus a\leq b\).
\(1\leq n,q\leq 10^5\), \(1\leq c_i\leq n\),\(a,b\leq n+1\)
思路
做法一
求区间内有多少种不同的权值?这不是经典问题嘛,先把询问离线下来,按照 \(r\) 从小到大排序,然后用线段树维护数字的出现情况,当 \(r\) 扫到值 \(c_i\) 时,先在线段树上 \(i\) 处 \(+1\),若 \(c_i\) 之前出现过,线段树上在之前出现的那个位置 \(-1\),然后直接线段树查询 \([l,r]\) 区间和即可。
这里采用类似的做法,在线段树每个节点上储存一棵 \(Trie\),表示对应区间上数字,在上传信息时,直接将两个儿子的 \(Trie\) 合并起来给父亲即可,由于一个数字会在 \(Trie\) 上产生 \(O(\log n)\) 个节点,而线段树有 \(O(\log n)\) 层,所以空间复杂度为 \(O(n\log^2n)\) 。在查询时找到 \([l,r]\) 对应的 \(O(\log n)\) 个节点,将 \(Trie\) 上的答案加起来即可。
\(Trie\) 上的询问很简单,传入两个参数 \(a,b\),在 \(Trie\) 上贴着 \(a\oplus b\) 走,若 \(b\) 当前位是 \(1\),则所有 \(k\oplus a\) 在当前位是 \(0\) 的数字都符合要求(前面的所有位置都与 \(a\oplus b\) 相同),即将对应节点的子树大小加入到答案中,最后再把 \(k=a\oplus b\) 的情况加到答案里即可,\(O(\log n)\) 。
综上,时间复杂度 \(O((n+q)\log^2 n)\),空间复杂度 \(O(n\log^2 n)\) 。
然而我们看一眼题目空间限制:\(256MB\),而 \(Trie\) 的每个节点需要储存 \(siz,leftson,rightson\) 三个 \(int\),满打满算需要 \(315.8MB\) 的内存,卡不过去!后来脑子清醒了一下,发现这里「单点加区间查询」的操作树状数组就可以做了,而根据树状数组的结构性质,它的空间复杂度具有 \(\frac{1}{2}\) 的常数(每一层的大小都为 \(\frac{n}{2}\)),所以上树状数组就行了!交上去实际使用内存 \(100.8MB\),然后就过了。
#pragma GCC optimize("O3")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,avx2,tune=native")
#pragma GCC optimize("inline","fast-math","unroll-loops","no-stack-protector")
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<fstream>
#include<ctime>
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 100100
#define LOG 17
#define lowbit(x) (x&-x)
using namespace std;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = (s<<3)+(s<<1)+(ch^48), ch = getchar();
return s*w;
}
int p[N], lst[N], ans[N];
int n, q;
struct node{
int siz, c[2];
} t[N*LOG*LOG/2]; //卡空间!
int cnt;
void insert(int x, int k, int id){
t[x].siz += id;
per(i,16,0){
int dir = k>>i&1;
if(!t[x].c[dir]) t[x].c[dir] = ++cnt;
x = t[x].c[dir];
t[x].siz += id;
}
}
int query(int x, int a, int b){
int ret = 0;
per(i,16,0){
int id = b>>i&1;
int dir = id^(a>>i&1);
if(id) ret += t[t[x].c[dir^1]].siz;
if(!t[x].c[dir]) break;
x = t[x].c[dir];
if(i == 0) ret += t[x].siz;
}
return ret;
}
struct Fenwik_Tree{
int t[N];
void add(int loc, int k, int id){
while(loc <= n) insert(t[loc], k, id), loc += lowbit(loc);
}
int query(int loc, int a, int b){
int ret = 0;
while(loc) ret += ::query(t[loc], a, b), loc -= lowbit(loc);
return ret;
}
} BIT;
struct query{
int l, r, a, b, id;
bool operator < (query c){ return r < c.r; }
} ask[N];
int main(){
n = read();
rep(i,1,n) p[i] = read();
q = read();
rep(i,1,q){
ask[i].l = read(), ask[i].r = read(), ask[i].a = read(), ask[i].b = read();
ask[i].id = i;
}
rep(i,1,n) BIT.t[i] = i;
cnt = n;
sort(ask+1, ask+q+1);
int now = 1;
rep(i,1,q){
while(now <= ask[i].r){
if(lst[p[now]]) BIT.add(lst[p[now]], p[now], -1);
BIT.add(now, p[now], 1);
lst[p[now]] = now;
now++;
}
ans[ask[i].id] = BIT.query(ask[i].r, ask[i].a, ask[i].b) - BIT.query(ask[i].l-1, ask[i].a, ask[i].b);
}
rep(i,1,q) printf("%d\n", ans[i]);
return 0;
}
做法二
做法一是「树状数组套 \(Trie\) 」,它时空复杂度同阶的原因是,树套树两层数据结构都是时空复杂度同阶的(树状数组拥有 \(O(\log n)\) 层,所以套起来就不是线性空间的),所以如果可以把其中一个换成线性空间的数据结构,如套在里面的树状数组,或者平衡树,空间复杂度就可以少一个 \(\log\),无需卡空间了。
若 \(Trie\) 在里面,外面的选择就只有平衡树,显然这没法做,那就把 \(Trie\) 放外面!考虑 \(Trie\) 套平衡树的做法,我们只建一棵 \(Trie\),\(Trie\) 上每个节点开一棵平衡树,维护有哪些下标的值会访问到当前节点,这里为了去重,也需要按照之前的策略,把询问离线下来按 \(r\) 排序,将每个值先前出现的位置在平衡树上删去,然后查询时直接在 \(Trie\) 上走,在合法的节点平衡树上找有多少节点的 \(val\geq l\),统计答案即可。
时间复杂度 \(O(n\log^2 n)\),空间复杂度 \(O(n\log n)\)。
实现细节
-
出题人毒瘤,他卡常。开始写了 \(fhqTreap\) 被卡的死死的,换成 \(rotate\) 版本的 \(Treap\) 才卡过去。
-
为了平衡空间,开始每棵 \(Treap\) 里都是用 \(vector\) 存节点的,本人的 \(Treap\) 是通过引用操作对节点进行修改信息的,然而 对 \(vector\) 的内容进行引用操作是 Undefined Behaviour,因为 \(vector\) 在改变长度时会重构空间,导致指向原地址的指针失效。
#pragma GCC optimize("O3")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,avx2,tune=native")
#pragma GCC optimize("inline","fast-math","unroll-loops","no-stack-protector")
#include<iostream>
#include<vector>
#include<cstdlib>
#include<ctime>
#include<algorithm>
#include<cstdio>
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 100100
#define LOG 17
#define Root -1
#define rt(x) t[x].p.root
#define Inf 0x3f3f3f3f
using namespace std;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = (s<<3)+(s<<1)+(ch^48), ch = getchar();
return s*w;
}
struct node{
int val, siz, dat;
int c[2];
node(){ val = siz = dat = c[0] = c[1] = 0; }
} t[2*N*LOG];
int cnt;
struct Treap{
int root;
int New(int val){
++cnt;
t[cnt].val = val, t[cnt].dat = rand(), t[cnt].siz = 1;
return cnt;
}
void print(int x){
if(x == Root) x = root;
if(x == 0) return;
print(t[x].c[0]), cout<<t[x].val<<" ", print(t[x].c[1]);
}
inline void pushup(int x){
t[x].siz = t[t[x].c[0]].siz + t[t[x].c[1]].siz + 1;
}
void init(){
root = New(-Inf), t[root].c[1] = New(Inf);
pushup(root);
}
void rotate(int &x, int id){
int u = t[x].c[id];
t[x].c[id] = t[u].c[id^1], t[u].c[id^1] = x, x = u;
pushup(t[x].c[id^1]), pushup(x);
}
void insert(int &x, int val){
if(x == 0){ x = New(val); return; }
if(val < t[x].val){
insert(t[x].c[0], val);
if(t[x].dat < t[t[x].c[0]].dat) rotate(x, 0);
} else{
insert(t[x].c[1], val);
if(t[x].dat < t[t[x].c[1]].dat) rotate(x, 1);
}
pushup(x);
}
void del(int &x, int val){
if(!x) return;
if(val == t[x].val){
if(t[x].c[0] || t[x].c[1]){
if(!t[x].c[1] || t[t[x].c[0]].dat > t[t[x].c[1]].dat){
rotate(x, 0), del(t[x].c[1], val);
} else rotate(x, 1), del(t[x].c[0], val);
pushup(x);
} else x = 0;
return;
}
if(val < t[x].val) del(t[x].c[0], val);
else del(t[x].c[1], val);
pushup(x);
}
int query(int x, int val){
if(x == Root) x = root;
if(x == 0) return 0;
if(t[x].val < val) return query(t[x].c[1], val);
else return t[t[x].c[1]].siz + 1 + query(t[x].c[0], val);
}
};
struct Trie{
struct node{
int c[2];
Treap p;
} t[N*LOG];
int cnt;
Trie(){ cnt = 1, t[1].p.init(); }
void insert(int k, int pos, int pre){
if(pre) t[1].p.del(rt(1), pre);
t[1].p.insert(rt(1), pos);
int x = 1;
per(i,16,0){
int dir = k>>i&1;
if(!t[x].c[dir]) t[x].c[dir] = ++cnt, t[cnt].p.init();
x = t[x].c[dir];
if(pre) t[x].p.del(rt(x), pre);
t[x].p.insert(rt(x), pos);
}
}
int query(int a, int b, int l){
int x = 1, ret = 0;
per(i,16,0){
int id = b>>i&1;
int dir = id^(a>>i&1);
if(id && t[x].c[dir^1]) ret += t[t[x].c[dir^1]].p.query(Root, l)-1;
if(!t[x].c[dir]) break;
x = t[x].c[dir];
if(i == 0) ret += t[x].p.query(Root, l)-1;
}
return ret;
}
} trie;
struct query{
int l, r, a, b, id;
bool operator < (query k){ return r < k.r; }
} ask[N];
int a[N], ans[N], lst[N];
int n, q;
int main(){
srand(time(0));
n = read();
rep(i,1,n) a[i] = read();
q = read();
rep(i,1,q){
ask[i].l = read(), ask[i].r = read(), ask[i].a = read(), ask[i].b = read();
ask[i].id = i;
}
sort(ask+1, ask+q+1);
int it = 1;
rep(i,1,q){
while(it <= ask[i].r)
trie.insert(a[it], it, lst[a[it]]), lst[a[it]] = it, it++;
ans[ask[i].id] = trie.query(ask[i].a, ask[i].b, ask[i].l);
}
rep(i,1,q) printf("%d\n", ans[i]);
return 0;
}
不过正如前面的分析,\(Trie\) 套树状数组也是可行的,不过要预处理每个节点可能会遇到的所有位置,并将它们各自离散化,然后 \(vector\) 存下标,时空复杂度同上。实现就咕了吧,应该不复杂。
做法三
我会莫队!嗯,讲完了。
时间复杂度 \(O(n\sqrt n\log n)\),空间复杂度 \(O(n\log n)\)
实现细节
虽说莫队就行了,不过 \(2s\) 的时限和 \(5.25e8\) 的运算量看起来不大能过,我们来大力卡常!
- 朴素的实现,按照 \(\{\frac{l}{\sqrt n},r\}\) 双关键字将询问排序,然后直接对 \(Trie\) 修改和查询:\(36s\)
- 上 \(pragma\) 火箭头优化:\(9.8s\)
- 用 \(num_i\) 记录数字 \(i\) 在 \([l,r]\) 的出现次数,若插入当前数字不会改变答案,就不插了:\(3.3s\)
- 把 \(cin\) 去同步换成快读:\(980ms\)
- 既然只有一棵 \(01Trie\),采用线段树的节点标号方式,上位运算:\(263ms\)
现在看起来非常稳了,交一发试试,出题人的数据真强,竟然跑了 \(1450ms\) 。
-
把排序标准换成:当 \(\frac{l}{\sqrt n}\) 相同时,若 \(\frac{l}{\sqrt n}\) 为奇数,返回 \(r_i\geq r_j\),否则返回 \(r_i\leq r_j\):\(268ms\)
这么做使得 \(r\) 的位置变化可以被充分利用,本身莫队的常数为 \(3\)(\(n\) 次 \(O(\sqrt n)\) 的 \(l\) 变化,发生在块内的 \(\sqrt n\) 次 \(O(n)\) 的 \(r\) 右移,发生在块间的 \(\sqrt n\) 次 \(O(n)\) 的 \(r\) 左移),这样常数就只有 \(2\) 了。
虽然本地慢了一点,但是交上去只要 \(967ms\) 了,系三种做法中最快的,经过卡常,代码在本地快了 \(137\) 倍,跑出了一秒 \(6e8\) 的神速!
#pragma GCC optimize("O3")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,avx2,tune=native")
#pragma GCC optimize("inline","fast-math","unroll-loops","no-stack-protector")
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstdio>
#include<fstream>
#include<ctime>
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 100010
#define B 316
#define LOG 17
using namespace std;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = (s<<3)+(s<<1)+(ch^48), ch = getchar();
return s*w;
}
int num[N], siz[N*LOG];
int cnt = 1;
void insert(int k, int id){
if(!k) return;
if(id == -1 && --num[k] > 0) return;
if(id == 1 && num[k]++ > 0) return;
int x = 1;
siz[x] += id;
per(i,16,0) x = (x<<1) + (k>>i&1), siz[x] += id;
}
int query(int a, int b){
int x = 1, ret = 0;
per(i,16,0){
int dir = (b>>i&1)^(a>>i&1);
if(b>>i&1) ret += siz[(x<<1) + (dir^1)];
x = (x<<1) + dir;
}
ret += siz[x];
return ret;
}
int a[N], ans[N];
int n, q;
struct query{
int l, r, a, b, id;
bool operator < (query k){ return (l/B != k.l/B ? l/B < k.l/B : (r < k.r) ^ ((l/B)&1)); }
} ask[N];
int main(){
n = read();
rep(i,1,n) a[i] = read();
q = read();
rep(i,1,q){
ask[i].l = read(), ask[i].r = read(), ask[i].a = read(), ask[i].b = read();
ask[i].id = i;
}
sort(ask+1, ask+q+1);
int l = 0, r = 0;
rep(i,1,q){
while(l > ask[i].l) insert(a[--l], 1);
while(r < ask[i].r) insert(a[++r], 1);
while(l < ask[i].l) insert(a[l++], -1);
while(r > ask[i].r) insert(a[r--], -1);
ans[ask[i].id] = query(ask[i].a, ask[i].b);
}
rep(i,1,q) printf("%d\n", ans[i]);
return 0;
}