纪念神九发射作业(1)
今天rabbithu和陈独秀……给我们留了八道提高+~NOI难度的题目……蒟蒻今天只做出来三道……还是别人给讲的……
不管怎么说,写题解记录下来……不然以后忘了这种题怎么做咋整……
C题:CQOI2018 异或序列
这道题可以看出来是莫队的板子题,不过一开始还是看得我一脸懵逼……
由于异或的逆运算是其本身,那么我们就可以想办法解决这道题中最关键的一点——如何在分块之后对于每个访问的区间都能O(1)的求出其内部异或和等于k的子区间有多少个。
基础的莫队是用桶来记录一个数出现了多少次,而且他是每次移动一个点,单点修改的,既然如此,那我们也可以仿照这种思路来写。
首先是如何处理一些不与访问区间端点相连的子区间的问题,这个仔细想想发现很容易,因为我们是一个一个点修改的,这样我们会记录下来所有的其异或前缀和^k符合条件的点。
再者,对于序列内任意一个数a[l],a[l] = sum[l-i]^sum[l],其中sum记录异或前缀和。再根据异或的逆运算是其本身的性质,我们直接在每次更新莫队的时候,把当前这个点的异或前缀和压到桶中,并且在总和上记录该异或前缀和再异或k的值在当前的莫队中有多少个即可。这样每次增加一个点,我们就相当于增加了一段异或区间和等于k的区间。删除与之道理相同。
最后还有一点,一开始的桶要初始化,把cnt[0]设为1,因为你在计算右端点为l的访问区间的时候,必须要用到l-1,所以我们先手压入一个0以正常工作。
上代码啦。
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<cmath> #include<queue> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') using namespace std; const int M = 100005; const int B = ceil(sqrt(M)); #define bel(x) ((x - 1) / B + 1)//分块 typedef long long ll; struct node { int l,r,id; bool operator < (const node &b) const//重载运算符进行排序 { return bel(l) == bel(b.l) ? r < b.r : l < b.l; } }q[M]; int read() { int ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } int n,m,k,a[M],cnt[M],sum,ans[M]; void insert(int x)//插入和删除 { sum += cnt[x^k]; cnt[x]++; } void del(int x) { cnt[x]--; sum -= cnt[x^k]; } int main() { n = read(),m = read(),k = read(); rep(i,1,n) a[i] = read(),a[i] ^= a[i-1]; rep(i,1,m) q[i].l = read(),q[i].r = read(),q[i].id = i; sort(q+1,q+1+m); int kl = 1,kr = 0;cnt[0] = 1;//这里是为了计算以1为开头的区间的前缀和 rep(i,1,m)//正常莫队操作,注意顺序 { while(kr < q[i].r) insert(a[++kr]); while(kr > q[i].r) del(a[kr--]); while(kl < q[i].l) del(a[kl-1]),kl++; while(kl > q[i].l) kl--,insert(a[kl-1]); ans[q[i].id] = sum; } rep(i,1,m) printf("%d\n",ans[i]); return 0; }
F题:luogu3801 红色的幻想乡
rabbithu说D题最简单……然而分明是F题最简单……
来了个贼强的无限放雾的dalao。这题一开始想到二维线段树(然而根本就不会),不过后来发现并不需要。因为这姐们每次从一个点放雾是向横,纵向无限长放雾,那么我们在维护某一向的值得时候,就不用维护这一向的每一点在另一方向上的长度了。
而且,她每次放雾是自己所在的位置没雾,而且两股雾在一起会抵消哦,也就是相当于她自己先在纵向上放一列,又在横向上放一行,结果自己内个位置因为放过两次抵消了,这样就可以用异或操作修改了。
到这里思路已经很清晰了,建两棵线段树,一棵维护纵向,一颗维护横向,那么怎么计算呢?
对于其给定的每一个矩形区域,是可以保证区域中只要有红雾的行、列一定充满了红雾,我们只要先query一遍这个矩形长方向上的红雾和,在query一遍纵向的,之后把横向上的乘以矩形纵向长,纵向乘以矩形横向长,最后再用容斥原理,减去二倍的交点数即可。(注意这里一定不要乘反!我就是乘反才爆零)
本题数据到10^5,别以为其平方小于2147483647就无忧了,你在相加的过程中是可能溢出的,要开longlong。之后就轻松A了。
上代码。
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<cmath> #include<queue> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') using namespace std; const int M = 100005; typedef long long ll; struct seg { ll v; }t1[M<<2],t2[M<<2]; ll read() { ll ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } ll n,m,q,op,x,y,kx,ky,sx,sy,l,w,k1,k2,d; void modify1(ll p,ll l,ll r,ll x)//这题真的只需要单点修改,modify2照抄 { if(l == r) { t1[p].v ^= 1; return; } ll mid = (l+r) >> 1; if(x <= mid) modify1(p<<1,l,mid,x); else modify1(p<<1|1,mid+1,r,x); t1[p].v = t1[p<<1].v + t1[p<<1|1].v; } void modify2(ll p,ll l,ll r,ll x) { if(l == r) { t2[p].v ^= 1; return; } ll mid = (l+r) >> 1; if(x <= mid) modify2(p<<1,l,mid,x); else modify2(p<<1|1,mid+1,r,x); t2[p].v = t2[p<<1].v + t2[p<<1|1].v; } ll query1(ll p,ll l,ll r,ll kl,ll kr)//query与普通线段树相同,query2照抄 { if(l == kl && r == kr) return t1[p].v; ll mid = (l+r) >> 1; if(kr <= mid) return query1(p<<1,l,mid,kl,kr); else if(kl > mid) return query1(p<<1|1,mid+1,r,kl,kr); else return query1(p<<1,l,mid,kl,mid) + query1(p<<1|1,mid+1,r,mid+1,kr); } ll query2(ll p,ll l,ll r,ll kl,ll kr) { if(l == kl && r == kr) return t2[p].v; ll mid = (l+r) >> 1; if(kr <= mid) return query2(p<<1,l,mid,kl,kr); else if(kl > mid) return query2(p<<1|1,mid+1,r,kl,kr); else return query2(p<<1,l,mid,kl,mid) + query2(p<<1|1,mid+1,r,mid+1,kr); } int main() { // freopen("a.in","r",stdin); n = read(),m = read(),q = read(); rep(i,1,q) { op = read(); if(op == 1) { x = read(),y = read(); modify1(1,1,n,x); modify2(1,1,m,y); } if(op == 2) { kx = read(),ky = read(),sx = read(),sy = read(); l = sx-kx+1,w = sy-ky+1;//计算矩形长宽 k1 = query1(1,1,n,kx,sx); k2 = query2(1,1,m,ky,sy); // printf("%d %d %d %d\n",l,w,k1,k2); d = k1 * w + k2 * l - 2 * k1 * k2;//用容斥原理计算被红雾覆盖块数 printf("%lld\n",d); } } return 0; }
H题:HEOI/TJOI2016 排序
这道题真的对我来说很有难度……如果不是听人讲的话真的做不出来……算法思路很好理解……不过有许多坑要注意。
听说这是一道套路题emm?
不管啦,直接开讲。这道题如果直接暴力模拟sort的话,是可以拿到30,那我们再想想,sort之所以不行的原因,是因为其每次操作nlogn,再乘以sort的次数必然不行,那么有什么方法能更快地排序呢……
显然普通方法是不可能的,那我们就有别的方法了……这是一个极好的套路,也是一个极为值得学习的方法——把所有数字转化为01串!
这样排序的问题就迎刃而解了。我们只需要先手统计出你要sort的区间有多少个1(k个),之后直接对起始~末尾-k和末尾-k+1~末尾做区间修改即可,全改成0或者1,贼舒服,升降序全搞定。每次sort只需要2logn的时间。
那么,我们接下来该干嘛呢……我们如何能通过01串来确定你要访问的位置上的数是多少,这是个关键。我们想,不过怎么排序,因为题目给定的排序只有一种顺序,照着他所说的排完你要的位置自然是答案,所以无论你是转化为01串还是什么其他的串,那个点上应该有的数是不会变的!那我们怎么用01串确定呢?
二分答案!每次把大于等于当前二分值得数设为1,小于的为0,这样的话sort结束之后如果要访问的位置如果是0的话那就说明你的数取大了(因为0映射一个小于当前二分值的数,而实际上那一位应该是1),否则就是取小或者正好等于,那么直接这么二分下去就可以了。算法思路十分清晰,二分答案之后每次新建一棵树,之后对其按照要求进行多次区间修改,最后返回要访问的那一位的值即可。二分logn,每次建树+区间修改mlogn,在n,m <= 10^5的情况下可以过。
但是这个题坑贼多。
1.你打lazy标记的时候,初始值要设为-1,因为你是直接赋值修改,而不是加,所以不想在加法操作中lazy'为0就可以随便操作,这里是不行的!
2.你modify的时候,由于query出来的1 的个数可能很多,有可能导致操作越界。
这时候你想,啊,那我直接去区间端点和要修改的端点的极值不就行了吗。不!你这样确实是不会RE,但是再想,比如你长度为4 的区间有四个1,你如果取端点,相当于你还是把一个端点值改为0,但你实际上是不应该改的!
所以我们的方法是在函数里加特判,其他啥也不要动!
3.线段树别写跪了……
就这些……然后就A了人生中第3道紫题。而且这个题的思路是十分值得学习的,这次理解了,也不知道以后能不能用出来……
上代码。
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<cmath> #include<queue> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') using namespace std; const int M = 100005; typedef long long ll; struct seg { int v; }t[M<<2]; struct opera { int l,r,op; }o[M]; int read() { int ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } int n,m,a[M],op,dl,dr,q,lazy[M<<2],mi,ans; void build(int p,int l,int r,int x)//建树 { if(l == r) { t[p].v = (a[l] >= x)? 1:0;//按大小来确定01 return; } int mid = (l+r) >> 1; build(p<<1,l,mid,x); build(p<<1|1,mid+1,r,x); t[p].v = t[p<<1].v + t[p<<1|1].v; } void down(int p,int l,int r)//注意lazy标记下放过程! { int mid = (l+r) >> 1; if(lazy[p] == -1) return; lazy[p<<1] = lazy[p]; lazy[p<<1|1] = lazy[p]; t[p<<1].v = lazy[p] * (mid-l+1); t[p<<1|1].v = lazy[p] * (r - mid); lazy[p] = -1; } int query(int p,int l,int r,int kl,int kr)//正常查询 { if(l == kl && r == kr) return t[p].v; down(p,l,r); int mid = (l+r) >> 1; if(kr <= mid) return query(p<<1,l,mid,kl,kr); else if(kl > mid) return query(p<<1|1,mid+1,r,kl,kr); else return query(p<<1,l,mid,kl,mid) + query(p<<1|1,mid+1,r,mid+1,kr); } void modify(int p,int l,int r,int kl,int kr,int val)//正常修改 { if(kl > kr || kl < l || kr > r) return;//这句话贼重要!少了就RE! if(l == kl && r == kr) { t[p].v = val * (r-l+1); lazy[p] = val; return; } down(p,l,r); int mid = (l+r) >> 1; if(kr <= mid) modify(p<<1,l,mid,kl,kr,val); else if(kl > mid) modify(p<<1|1,mid+1,r,kl,kr,val); else modify(p<<1,l,mid,kl,mid,val),modify(p<<1|1,mid+1,r,mid+1,kr,val); t[p].v = t[p<<1].v + t[p<<1|1].v; } bool check(int x) { memset(t,0,sizeof(t)); memset(lazy,-1,sizeof(lazy)); build(1,1,n,x);//先手建树 rep(i,1,m) { int k = query(1,1,n,o[i].l,o[i].r);//查询区间中1的个数 if(!o[i].op)//升序排列 { modify(1,1,n,o[i].l,o[i].r-k,0); modify(1,1,n,o[i].r-k+1,o[i].r,1);//这里注意不要乱改端点值,加特判即可 } else//降序 { modify(1,1,n,o[i].l,o[i].l+k-1,1); modify(1,1,n,o[i].l+k,o[i].r,0); } } return query(1,1,n,q,q);//返回单点查询值 } int main() { n = read(),m = read(); rep(i,1,n) a[i] = read(); rep(i,1,m) o[i].op = read(),o[i].l = read(),o[i].r = read();//离线操作 q = read(); dl = 1,dr = n; while(dl <= dr)//二分解决 { mi = (dl + dr) >> 1; if(check(mi)) dl = mi+1,ans = mi; else dr = mi-1; } printf("%d\n",ans); return 0; }