国庆中秋假期训练

版权声明:抱最大的希望,为最大的努力,做最坏的打算。 https://blog.csdn.net/qq_37748451/article/details/78170058

这几天真的是很赶,走亲戚出去吃饭什么的,一浪费就是一天一下午的时间。。。。。。。

也着急做题,,,,,,而且还都是些没接触的东西,加上线段树和树状数组这块学的不是很好。。。。

第一个就是 传说中能解决一切区间问题的莫队算法,这个算法早就想看一直拖到现在。

用来处理一类无修改的离线区间询问问题

问题:有n个数组成一个序列,有m个形如询问L, R的询问,每次询问需要回答区间内至少出现2次的数有哪些。

/*作者:张瑯小强
链接:https://www.zhihu.com/question/27316467/answer/130423804
*/
一种直观的办法是按照左端点排序,再按照右端点排序。但是这样的表现不好。特别是面对精心设计的数据,这样方法表现得很差。

举个例子,有6个询问如下:(1, 100), (2, 2), (3, 99), (4, 4), (5, 102), (6, 7)。  

这个数据已经按照左端点排序了。用上述方法处理时,左端点会移动6次,右端点会移动移动98+97+95+98+95=483次。

右端点大幅度地来回移动,严重影响了时间复杂度——排序的复杂度是O(mlogm),所有左端点移动次数仅为为O(n),但右端点每个询问移动O(n),共有m个询问,故总移动次数为O(nm),移动总数为O(mlogm + nm)。运行时间上界并没有减少。  

其实我们稍微改变一下询问处理的顺序就能做得更好:(2, 2), (4, 4), (6, 7), (5, 102), (3, 99), (1, 100)。  

左端点移动次数为2+2+1+2+2=9次,比原来稍多。右端点移动次数为2+3+95+3+1=104,右端点的移动次数大大降低了。  

上面的过程启发我们:

①我们不应该严格按照升序排序,而是根据需要灵活一点的排序方法;

②如果适当减少右端点移动次数,即使稍微增多一点左端点移动次数,在总的复杂度上看划算。

在排序时,我们并不是按照左右端点严格升序排序询问,而只是令其左右端点处于“大概是升序”的状态。

具体的方法是,把所有的区间划分为不同的块,将每个询问按照左端点的所在块序号排序,左端点块一样则按照右端点排序。

这就是莫队算法!!!!!!!!!

莫队算法首先将整个序列分成√n个块(同样,只是概念上分的块,实际上我们并不需要严格存储块),接着将每个询问按照块序号排序(一样则按照右端点排序)。之后,我们从排序后第一个询问开始,逐个计算答案。

题意已知一个长度为n的数列 (0 ≤ ai ≤ 1 000 000) ,给m个区间,问每个区间有多少个子区间xor和为k 
(1 ≤ n, m ≤ 100 000, 0 ≤ k ≤ 1 000 000)
莫队算法 
如果你知道了[L,R]的答案。你可以在O(1)的时间下得到[L,R-1]和[L,R+1]和[L-1,R]和[L+1,R]的答案的话。就可以使用莫队算法。
先对序列分块。然后对于所有询问按照L所在块的大小排序。如果一样再按照R排序。然后按照排序后的顺序计算。复杂度O(n^1.5)
对于本题
我们设定 sum[i] 为[1, i]区间内的异或和,对于区间[a, b]的异或和为sum[b] ^ sum[a-1]。如果区间 [a, b] 的异或和为k,则有sum[b] ^ sum[a-1] == k,由于异或的性质可以推论 出:sum[b] ^ k == sum[a-1],sum[a-1] ^ k == sum[b]。


需要注意的几个地方
1 结果可能超int
2 区间[i,j]的异或和是sum[i-1]^sum[j]的结果,所以要保存i-1到j的异或值
3 l和r以及flag[0]的初值,flag[i]代表前缀和的数量
4 add()和dele()函数的写法

  1. #include <cmath>  
  2. #include <queue>  
  3. #include <cstdio>  
  4. #include <cstring>  
  5. #include <cstdlib>  
  6. #include <iostream>  
  7. #include <algorithm>  
  8. #define LL long long  
  9. #define INF 0x3f3f3f3f  
  10. using namespace std;  
  11. const int maxn = 2e6+7;  
  12. struct node {int l,r,id;}q[maxn];  
  13. int a[maxn],pos[maxn];  
  14. LL ans[maxn],sum[maxn];  
  15. int n,m,k;  
  16. LL Ans=0;  
  17. bool cmp(node a,node b)  
  18. {  
  19.     if(pos[a.l]!=pos[b.l]) return pos[a.l]<pos[b.l];  
  20.     return a.r<b.r;  
  21. }  
  22. void add(int x)  
  23. {  
  24.     Ans += flag[a[x]^k];  
  25.     flag[a[x]]++;  
  26. }  
  27. void dele(int x)  
  28. {  
  29.     <span style="font-family:Arial, Helvetica, sans-serif;">flag[a[x]]--;</span>  
  30.     Ans -= flag[a[x]^k];  
  31. }  
  32. int main()  
  33. {  
  34.     int i;  
  35.     while(~scanf("%d%d%d",&n,&m,&k))  
  36.     {  
  37.         Ans=0;memset(flag,0,sizeof(flag));  
  38.         int ss=sqrt(n);flag[0]=1;  
  39.         for(i=1;i<=n;i++) scanf("%d",a+i),a[i]^=a[i-1],pos[i]=i/ss;  
  40.         for(i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;  
  41.         sort(q+1,q+1+m,cmp);  
  42.         int l=1,r=0;  
  43.         for(i=1;i<=m;i++)  
  44.         {  
  45.             while(q[i].l<l){l--;add(l-1);}  
  46.             while(q[i].l>l){dele(l-1);l++;}  
  47.             while(q[i].r<r){dele(r);r--;}  
  48.             while(q[i].r>r){r++;add(r);}  
  49.             ans[q[i].id]=Ans;  
  50.         }  
  51.         for(i=1;i<=m;i++) printf("%I64d\n",ans[i]);  
  52.     }  
  53.     return 0;  
  54. }  

还有就是线段树离散化,有关离散化的题我是放在后面做的

对于离散化,初步理解就是因为给的范围无限大或过大无法用数组直接表示,所以进行转换

eg 范围[1,6] [1.7] [2,10] [8 18] 将各点排序
1 1 2 6 7 8 10 18   离散后对应的坐标为
 1  2 3 4 5 6  7    再根据原来的点把它们对应起来,则离散后坐标为
[1,3] [1,4] [2,6] [5,7]
求解这题的方法 是从后往前贴 如果发现这块区域被完全覆盖了,那就返回。

离散化是一种常用的技巧,有时数据范围太大,可以用来放缩到我们能处理的范围 因为其中需排序的数的范围0---
999999999;显然数组不肯能这么大;而N的最大范围是500000;故给出的数一定可以与1.。。。N建立一个一一映射;
 
这里用一个结构体
struct Node
 {
      int v,order;
 }a[510000];
和一个数组aa[510000];
 
其中v就是原输入的值,order是下标;然后对结构体按v从小到大排序;此时,v和结构体的下标就是一个一一对应关系,而且
满足原来的大小关系;
for(i=1;i<=N;i++)
     aa[a[i].order]=i;
然后a数组就存储了原来所有的大小信息;比如 9 1 0 5 4 ------- 离散后aa数组就是 5 2 1 4 3;具体的过程可以自己
用笔写写就好了。
离散之后,怎么使用离散后的结果数组来进行树状数组操作? 
如果数据不是很大,可以一个个插入到树状数组中,每插入一个数,统计比他小的数的个数,对应的逆序为 i- getsum(aa
[i]),其中i为当前已经插入的数的个数,getsum(aa[i])为比aa[i]小的数的个数,i- sum(aa[i]) 即比aa[i]大的个数,
即逆序的个数但如果数据比较大,就必须采用离散化方法。

树状数组用于非前缀区间技巧

在一些情况下,可以使用树状数组来完成线段树的功能。
以RMQ问题为例,很容易想到的方法是,先按原始方式建立树状数组,对于一个元素bit[x],首先看x-lowbit(x)+1超没超出查询范围的左边界,没超出当然是最好了,因为这就跟一般树状数组一样处理就行了,直接使用这个值;但若是超出了,那肯定不能用这个元素了,没办法那就退一步吧,直接使用原数组的值,然后问题中的右边界不久小了1吗?虽然只少了1,但至少还是少了吧,然后接下来就是重复这个过程直到完全查询完整个区间(只要x-lowbit(x)+1刚好等于左边界就行了)。这样,树状数组也就不再依赖于区间减法操作,能够完成一些更复杂的操作,在某些场合替代线段树。
至于复杂度分析,不用说该是比lgN大了,因为遇到超出左边界的情况只能把右边界减1,这显然比起原始的树状数组就慢了,不过也没事。

。。。。。。。。哇

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页