[NOI Online 2021 提高组] 岛屿探险
前言
说实话,我并不知道我用的这个做法叫什么,我开始以为他是整体二分,然后发现我写出来的东西可能是线段树分治(然而我根本就不知道线段树分治是个啥)。
而且这个做法可能被卡,因为我本题测出来极限数据为2.9s,卡常后为2.4s。不过这份代码暂时可以在洛谷上过民间数据。
update 2021.4.4 出分了,只有40pts,暂时不知道哪里出了问题,等数据出来。
update 2021.8.17 偶然点开以前的文章,感觉好像是线段树分治。(不过锅是找不到了,数据没公开)
题目
讲解
本来这道题我是一点都不会的,但是我们完全可以根据套路把这道题做出来,我直接把草稿搬上来(部分删改):
显然这道题要离线。
由于这个 \(\tt min\) 很不好搞,猜测是对 \(d\) 进行排序,然后用数据结构维护 \(a\oplus c\) ,最后分别算最小值是 \(b\) 或者 \(d\) 的两个部分的贡献。
考虑把询问丢到线段树上面,也就是把每个询问分成约 \(log_2n\) 个,放到线段树上,这样我们如果可以处理出每个区间对应的询问,那么我们就可以做到 \(O(n\log_2n\log_2w)\) 了。
之后的讲解如果提到全部询问等字眼,指的是线段树中某个区间的全部询问。
这里我把取等条件是算在 \(d\) 里面的。
把式子写在这里:\(a_i\oplus c \le min(b_i,d)\) (\(c,d\)没有下标相当于我们考虑信息多组,询问为一组)。
part1 \(d \le b_i\)
这个部分要简单一些,我们只需要把所有的 \((a_i,b_i)\) 按 \(b_i\) 排序,然后把满足 \(d\le b_i\) 的 \(a_i\) 扔到字典树里面去。
显然所有询问 \((c,d)\) 也需要排序,用 \(two-pointer\) 维护插入的信息。
对于每个询问我们把一组 \((c,d)\) 扔进去跑即可。
part2 \(b_i < d\)
在上一个部分我们根据 \(a_i,c,d\) 可以算出有多少个位置满足条件,但是现在我们的信息是 \(a_i,b_i,c\),存在两个变元,这个又怎么做呢?
emmm,知3求2,我们可以把 \(a_i,b_i\) 扔到字典树里面去,显然我们可以推出 \(c\) 在某个范围是可以得到贡献的,最后把 \(c\) 放进去跑,累加答案即可。
当然这里我们依然要对 \((a_i,b_i)\) 排序,只是这个时候要扔到字典树里面的是 \(b_i < d\) 的 \((a_i,b_i)\)。
排序之后依然可以用 \(\tt two-pointer\) 维护插入的信息,但是这里我们发现上面插入的和这里插入的信息互为补集,所以我们需要先把part1的信息全部插入,然后按顺序插入part2的信息,删除part1的信息。
具体的,对于这棵字典树,我们这样操作(\(a\) 是 \(a_i\) 二进制下当前位置的数字,\(b,c\) 同理):
更新字典树的过程是抵着上限的(仔细揣摩这句话),也就是说在之前 \(a_i\) 和 \(b_i\) 是相等的。
\(a=0,b=0\),\(c\) 必为 \(0\),即走 \(0\) 边。
\(a=0,b=1\),此时如果 \(c\) 为 \(0\),显然可以直接获得贡献,在 \(0\) 边加个1,那么走 \(1\) 边继续更新。
\(a=1,b=0\),\(c\) 必为 \(1\),即走 \(1\) 边。
\(a=1,b=1\),此时如果 \(c\) 为 \(1\),显然可以直接获得贡献,在 \(1\) 边加个1,那么走 \(0\) 边继续更新。
这个地方的讲解可能有些复杂,建议与代码一起食用。
void ins1(Island x)
{
int now = 0;
for(int i = 23;i >= 0;-- i)
{
if(!t1[now].ch[0]) t1[now].ch[0] = ++tot1;
if(!t1[now].ch[1]) t1[now].ch[1] = ++tot1;
int aa = (x.a >> i) & 1,bb = (x.b >> i) & 1;
if(!aa)
{
if(!bb) now = t1[now].ch[0];
else t1[t1[now].ch[0]].cnt++,now = t1[now].ch[1];
}
else
{
if(!bb) now = t1[now].ch[1];
else t1[t1[now].ch[1]].cnt++,now = t1[now].ch[0];
}
if(!i) t1[now].cnt++;//走到最后还是相等的,不要忘了这个是要算贡献的。
}
}
至此我的做法已经全部讲完了,最后提醒一句,数据结构一定要对拍,而且记得跑大数据,多个机房大佬因为树套树空间等问题爆炸。
代码
考场代码,去调试信息版本,为了卡常我都改成归并排序了QAQ。(丑陋)
为了讲解更顺畅,我调换了part1,2的顺序,在代码中ins1,query1其实属于part2,ins2,query2属于part1。
//12252024832524
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
#define TT template<typename T>
using namespace std;
typedef long long LL;
const int MAXN = 100005;
int n,Q,A[MAXN],B[MAXN],ans[MAXN];
LL Read()
{
LL x = 0,f = 1;char c = getchar();
while(c > '9' || c < '0'){if(c == '-')f = -1;c = getchar();}
while(c >= '0' && c <= '9'){x = (x*10) + (c^48);c = getchar();}
return x * f;
}
TT void Put1(T x)
{
if(x > 9) Put1(x/10);
putchar(x%10^48);
}
TT void Put(T x,char c = -1)
{
if(x < 0) putchar('-'),x = -x;
Put1(x); if(c >= 0) putchar(c);
}
TT T Max(T x,T y){return x > y ? x : y;}
TT T Min(T x,T y){return x < y ? x : y;}
TT T Abs(T x){return x < 0 ? -x : x;}
struct Query
{
int c,d,ID;
bool operator < (const Query &px)const{
return d < px.d;
}
}q[MAXN];
vector<Query> t[MAXN<<2];
int now;
#define lc (x<<1)
#define rc (x<<1|1)
void insq(int x,int l,int r,int ql,int qr)
{
if(ql <= l && r <= qr)
{
t[x].push_back(q[now]);
return;
}
int mid = (l+r) >> 1;
if(ql <= mid) insq(lc,l,mid,ql,qr);
if(mid+1 <= qr) insq(rc,mid+1,r,ql,qr);
}
int tot1,tot2;
struct Trie
{
int ch[2],cnt;
}t1[MAXN * 48],t2[MAXN * 48];//b_i小于d & d小于等于b
struct Island
{
int a,b;
Island(){}
Island(int a1,int b1){
a = a1;
b = b1;
}
bool operator < (const Island &px)const{
return b < px.b;
}
}I[MAXN],I2[MAXN];
void ins1(Island x)
{
int now = 0;
for(int i = 23;i >= 0;-- i)
{
if(!t1[now].ch[0]) t1[now].ch[0] = ++tot1;
if(!t1[now].ch[1]) t1[now].ch[1] = ++tot1;
int aa = (x.a >> i) & 1,bb = (x.b >> i) & 1;
if(!aa)
{
if(!bb) now = t1[now].ch[0];
else t1[t1[now].ch[0]].cnt++,now = t1[now].ch[1];
}
else
{
if(!bb) now = t1[now].ch[1];
else t1[t1[now].ch[1]].cnt++,now = t1[now].ch[0];
}
if(!i) t1[now].cnt++;
}
}
void ins2(int x,int val)
{
int now = 0;
for(int i = 23;i >= 0;-- i)
{
int to = (x >> i) & 1;
if(!t2[now].ch[to]) t2[now].ch[to] = ++tot2;
now = t2[now].ch[to];
t2[now].cnt += val;
}
}
int query1(int x)
{
int now = 0,ret = 0;
for(int i = 23;i >= 0;-- i)
{
int to = (x >> i) & 1;
if(!t1[now].ch[to]) return ret;
now = t1[now].ch[to];
ret += t1[now].cnt;
}
return ret;
}
int query2(int c,int d)
{
int now = 0,ret = 0;
for(int i = 23;i >= 0;-- i)
{
if(!now && i != 23) return ret;
int cc = (c >> i) & 1,dd = (d >> i) & 1;
if(!dd)
{
if(!cc) now = t2[now].ch[0];
else now = t2[now].ch[1];
}
else
{
if(!cc) ret += t2[t2[now].ch[0]].cnt,now = t2[now].ch[1];
else ret += t2[t2[now].ch[1]].cnt,now = t2[now].ch[0];
}
if(!i && now) ret += t2[now].cnt;//走到最后还是相等,记得累加
}
return ret;
}
void dfs(int x,int l,int r)
{
int mid = (l+r) >> 1;
if(l != r) dfs(lc,l,mid),dfs(rc,mid+1,r);
if(l == r) I[l] = Island(A[l],B[l]);
int i1 = l,j1 = mid+1,k1 = l;
while(i1 <= mid && j1 <= r)
if(I[i1] < I[j1]) I2[k1++] = I[i1++];
else I2[k1++] = I[j1++];
while(i1 <= mid) I2[k1++] = I[i1++];
while(j1 <= r) I2[k1++] = I[j1++];
int now = l;
for(int i = l;i <= r;++ i) I[i] = I2[i],ins2(I[i].a,1);//插入信息互为补集,我们要提前插入part1的信息
for(int i = 0,len = t[x].size();i < len;++ i)
{
while(now <= r && I[now].b < t[x][i].d) ins1(I[now]),ins2(I[now].a,-1),now++;//插入part2的信息,删除part1的信息
ans[t[x][i].ID] += query1(t[x][i].c) + query2(t[x][i].c,t[x][i].d);
}
for(int i = 0;i <= tot1;++ i) t1[i].ch[0] = t1[i].ch[1] = t1[i].cnt = 0; tot1 = 0;
for(int i = 0;i <= tot2;++ i) t2[i].ch[0] = t2[i].ch[1] = t2[i].cnt = 0; tot2 = 0;
}
int lq[MAXN],rq[MAXN];
void solve2()
{
for(int i = 1;i <= Q;++ i)
{
lq[i] = Read(),rq[i] = Read();
q[i].c = Read(),q[i].d = Read(),q[i].ID = i;
}
sort(q+1,q+Q+1);
for(int i = 1;i <= Q;++ i) now = i,insq(1,1,n,lq[q[i].ID],rq[q[i].ID]);
dfs(1,1,n);
for(int i = 1;i <= Q;++ i) Put(ans[i],'\n');
}
int main()
{
// freopen("island.in","r",stdin);
// freopen("island.out","w",stdout);
n = Read(); Q = Read();
for(int i = 1;i <= n;++ i) A[i] = Read(),B[i] = Read();
solve2();
return 0;
}
By the way
有没有大佬告诉我这个做法是不是线段树分治啊。