zoukankan      html  css  js  c++  java
  • 从几场模拟考试看一类分块算法

    退役前最后一篇博客。。。希望我滚回去高考完还能记得一些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
    ",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问题中,有兴趣的可以学一下

  • 相关阅读:
    Android 废弃方法属性解决
    Android RecycleView分组
    Android 第三方库FlycoTabLayout
    Android 自定义dialog出现的位置
    Android 底部弹窗实现
    Android 自定义设置布局
    Android 微信、qq分享文本 (Intent)
    SpringBoot关于跨域的三种解决方案
    记录一次通用Mapper+自定义mapper出现的问题分析以及排查
    IDEA配置Maven+新建Maven项目
  • 原文地址:https://www.cnblogs.com/1-1-1-1/p/8761775.html
Copyright © 2011-2022 走看看