您的位置:首页 > 其它

从几场模拟考试看一类分块算法

2018-04-10 08:08 120 查看

退役前最后一篇博客。。。希望我滚回去高考完还能记得一些OI知识

 

最近考试我们有一道这样的题目:

如果有两个序列A和B,对于A序列中的每一个元素,都在B序列中选一个大于等于它的最小的值。若A序列选出的不同的B序列元素个数为奇数则称之为好的。现在给出n个序列S1..n,对于每两个序列Si,Sj(i<j)进行一次判断,问总共有多少个好的序列对。 n<=5000,A,B序列的元素大小不超过50。

 

首先std给出了这样的题解:

因为所有的位置范围是[1,50],我们可以用位运算加速统计。将序列 b 转换成一个二进制数B,将序列 a 转换成一个二进制数 A,我们要统计的是 A 中的某些特定区间内是否有二进制 1。令B ′ = B ≪ 1,则B ′ &(A + (A|~B ′ ))中的二进制 1 即表示A的对应区间内是否有 1,统计其二进制 1 的奇偶性即可。

 

看起来十分简洁但是对我这种NOIP(普及组初赛)水平选手却并不容易想到,因此考试的时候我得到了一个不怎么优的做法:分块

 

分析:

首先我们可以发现一共有C(n,2)种选法,这个数量大约是1.25E7。那么对于每个的判断我们有8次操作的时间完成。

问题显然可以转化为二进制,那么我们可以得到一个二进制下的50位数。

位数不多,情况只有01两种,所以考虑采用分块。

令块的大小为S,那么在块内我们总共有4^S种方案,每种方案我们可以在O(S)的时间内处理出它填到了块内B序列中的哪些位置,以及是否有剩余的没填的。顺便对于每一种B序列的块,处理左边第一个1在哪里。

换句话说,预处理的时间复杂度为O(4^S*S)。

对于每两个序列,预先分好块,不足的补满,最大块数为5块。从左到右扫描每一块,并查表得到答案,并和上一个的答案合并,合并的时候要考虑上一块是否多余了一些。

可以发现每一次询问的时间复杂度为O(m/S),共O(n^2)组询问,总时间O(n^2*m/S)。

总的来说,时间复杂度为O(4^S*S+n^2*m/S)。

 

分析分块大小,通过解方程我们知道S=10时比较优,实测可以过,最大数据0.2秒。

 

奉上代码:

1 #include<bits/stdc++.h>
2 using namespace std;
3
4 int n;
5 int num[5010];
6 int a[5010][53];
7 long long res[5010][7];
8 long long pp[1050][1050];
9 long long rm[1050][1050];
10 int frt[1050];
11
12 void read(){
13     scanf("%d",&n);
14     for(int i=1;i<=n;i++){
15         scanf("%d",&num[i]);
16         for(int j=1;j<=num[i];j++){
17             scanf("%d",&a[i][j]);
18         }
19         for(int j=1;j<=num[i];j++){
20             res[i][0] |= (1ll<<a[i][j]-1);
21         }
22         for(int j=1;j<=5;j++){
23             for(int k=0;k<10;k++){
24                 if((1ll<<(j-1)*10+k)&res[i][0]){
25                     res[i][j] |= (1<<k);
26                 }
27             }
28         }
29     }
30 }
31
32 void deal(int tot,int tt){
33     int j = 0; for(;j<10;j++) if(tt&(1<<j)) break;
34     for(int i=0;i<10;i++){
35         if((tot&(1<<i)) && j < 10) pp[tot][tt] |= (1<<j);
36         if((tot&(1<<i)) && j >= 10) rm[tot][tt] = 1;
37         if(i+1 > j){ j++; while(j < 10){if(tt&(1<<j))break; j++;} }
38     }
39 }
40
41 void dfs2(int now,int tot,int tt){
42     if(now > 10){ deal(tot,tt); }
43     else{
44         dfs2(now+1,tot,tt|(1<<now-1));
45         dfs2(now+1,tot,tt);
46     }
47 }
48
49 void dfs1(int now,int tot){
50     if(now > 10){
51         for(int i=0;i<=10;i++){if((tot&(1<<i))||i==10){frt[tot] = i;break;}}
52         dfs2(1,tot,0);
53     }else{
54         dfs1(now+1,tot|(1<<now-1));
55         dfs1(now+1,tot);
56     }
57 }
58
59 int anss = 0;
60 void solve(int l,int r){
61     long long om = 0,ro = 0;
62     for(int i=1;i<=5;i++){
63         om = om+(pp[res[l][i]][res[r][i]]<<10*(i-1));
64         if(ro && frt[res[r][i]] != 10){
65             int ft = frt[res[r][i]];
66             om |= (1ll<<ft+10*(i-1));
67             ro = 0;
68         }
69         ro |= rm[res[l][i]][res[r][i]];
70     }
71     anss += __builtin_parityll(om);
72 }
73
74 void work(){
75     dfs1(1,0);
76     for(int i=1;i<=n;i++){
77         for(int j=i+1;j<=n;j++){
78             solve(i,j);
79         }
80     }
81     printf("%d\n",anss);
82 }
83
84 int main(){85     read();
86     work();
87     return 0;
88 }

 

更进一步地灵活运用它,我们再来看一道题。

维护一个序列,支持单点修改,区间和的phi值查询以及区间乘法的phi值查询。其中序列长度n为50000,值的大小ai为40000,询问个数q为100000

 

题解给出了一种基于二分的方法,但是对我这种NOIP(普及组初赛)水平选手却并不容易想到,因此考试的时候我得到了一个不怎么优的做法:分块。

 

对于加法我们比较容易想到一个N^1.5的枚举质因数的做法。所以我们只考虑修改和乘法。

考虑从phi的定义入手,那么我们只需要考虑l到r之间哪些素数是出现过的,由于ai在40000以内,所以我们发现质数个数接近40000/ln(40000)个。

使用bitset维护合并,建一棵线段树维护区间内的bitset的并集。

这样子,单次修改操作会改变logn个bitset的内容,时间复杂度为O( (logn*ai) / (ln(ai)*w) ),我们可以认为ai和n同阶,时间复杂度为O(n/w).

考虑每次区间乘法操作,需要合并logn个bitset,这一部分时间复杂度同上。

另外的,我们需要对合并结果进行处理,也就是对bitset的每个位对应的p都处理一次,这个操作的时间复杂度O(n/ln(n)).

当三种操作平均分布的时候,我们的时间复杂度为O(q*(sqrt(n)+n/w+n/ln(n))).

 

这个算法有优化的空间,我们发现n/ln(n)是我们算法速度的瓶颈。带入数字可以发现,当操作不随机的时候,我们可以构造一个数据,使得总运行次数达到3.7E+8次。其中光是n/ln(n)就占了3E+8。

 

实际上,我们有大量的运算是重复的。

下面我举一个例子。

考虑01串

1011011100001010

1011010100011010

我们将它按每4位算一组,当我们计算这两个01串的时候,我们要计算8次。但如果我们预处理出了每4位对应的答案,那么我们只要计算6次。

这在q很少的时候优化不明显,但是当q的级别很大的时候,那么这样可以节省很多运算。

对于上面的2操作得到的最终的并,我们可以将它每16位分为一组,通过DP预处理出答案。DP的转移是O(1)的,所以DP预处理的时间复杂度为O(n*2^16/16)。

接着我们对于最终的并,每16位作为一个整体在表中直接查询答案,时间复杂度可以在原来的基础上除以16,令16=v,2操作的时间复杂度被降低为O(n/w+n/ln/v),同样的数据只用计算1E+8次。

 

值得注意的是,这并不是一个玄学的优化,而是一类有体系的优化方法。

 

这种方法同样也被运用在RMQ问题中,有兴趣的可以学一下

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: