zoukankan      html  css  js  c++  java
  • 巧妙运用位运算

    本文旨在通过两道巧妙运用位运算的题,认识位运算的魅力

    题目一

    题意:
    给定两个序列(A,B),求(A,B)的最长公共子序列
    (|A|,|B|le 10^5)
    时限:(5s)

    目前求任意两序列的最长公共子序列,是没有复杂度低于(O(|A|cdot |B|))的算法的

    回顾经典的(O(|A|cdot |B|))

    [f_{i,j}=max(f_{i-1,j},f_{i,j-1},f_{i-1,j-1}+[A_i+B_j]) ]

    显然有如下性质:

    [egin{aligned} &f_{i,j-1}le f_{i,j}\ &f_{i-1,j-1}le f_{i,j}\ &f_{i,j}-f_{i,j-1}le 1\ &f_{i,j}-f_{i-1,j}le 1 end{aligned}]

    (M_{i,j}=f_{i,j}-f_{i,j-1})
    考虑转移
    我们将( ext{M}_{i-1})划分为若干段,每段以一个(1)结尾:

    [000...01][000...01][000...0]
     ^^                        ^
     12                       |B|
    

    考虑(A_i)出现在(B)中的位置集合,令其为( ext{Ab})
    ( ext{X=M}_{i-1}| ext{Ab})
    对于( ext{M}_{i-1})的每一段,保留( ext{X})最前面的一个(1),则为( ext{M}_i)

    例如

    $M_{i-1}$=[000...01][000...01][000...0]
         $Ab$=[110...00][000...00][001...0]
        $M_i$=[100...00][000...01][001...0]
    

    我们已知了转移方式,考虑如何通过位运算得到

    对于某一段如:X=[0011001]
    考虑前导0变成1,第一个1变成0:[1101001]
    异或上X,得到[1110000]
    在按位与上X,得到[0010000]
    

    我们将( ext{X})翻转,第一步操作变成

    X=[1001100]
    [1001101]
    

    这个意义是(-1),这是一段的操作,对所有段如何快速进行呢
    发现我们将( ext{M}_{i-1})整体翻转后
    再左移,给最右边填上(1),效果为:

    $M_{i-1}$=[000...0][100...0][100...0]
               ^                      ^^
              |B|                     21
              [000...1][000...1][000...1]
    这恰好使得每一段都变成1
    

    那么直接减即可,具体细节见代码
    将矩阵的行压成(K)块,即可实现(O(|A|cdot K))的时间复杂度
    以下代码在(n,m=10^5),不开任何优化的情况下,跑进1s

    #include<bits/stdc++.h>
    typedef int LL;
    typedef unsigned long long ull;
    const LL maxn=1e5+9;
    LL Read(){
    	LL x(0),f(1); char c=getchar();
    	while(c<'0' || c>'9'){
    		if(c=='-') f=-1; c=getchar();
    	}
    	while(c>='0' && c<='9'){
    		x=(x<<3ll)+(x<<1ll)+c-'0'; c=getchar();
    	}return x*f;
    }
    LL n,m,ans;
    LL a[maxn],b[maxn];
    ull dp[maxn/63],pos[maxn][maxn/63];
    int main(){
    	n=Read(); m=Read();
    	for(LL i=0;i<n;++i){
    		a[i]=Read();
    	}
    	for(LL i=0;i<m;++i){
    		b[i]=Read();
    	}
    	for(LL i=0;i<m;++i){
    		pos[b[i]][i/63]|=1ull<<i%63;
    	}
    	LL up=(m-1)/63;
    	for(LL i=0;i<n;++i){
    		LL x(a[i]);
    		ull pre(1ull<<63);
    		for(LL j=0;j<=up;++j){
    			ull a(dp[j]),c(a|pos[x][j]|1ull<<63);
    			ull tmp(((c-(a<<1ull|1))^c)&c);
    			if(pre<(1ull<<63)){
    				tmp-=tmp&(-tmp);
    			}
    			pre=tmp;
    			dp[j]=tmp&((1ull<<63)-1);
    		}
    		if(pre<(1ull<<63)) ++ans;
    	}
    	printf("%d
    ",ans);
    }
    

    题目二

    题意
    给定(n)个点,第(i)个点为((i,p_i))(序列({p_i})({1,2,cdots,n})的某个排列)
    每个点还有一个颜色(c_i)
    (q)次询问,每次给定(l,r,d,u),求集合({i|iin[l,r],p_iin[d,u]})的颜色个数
    (n,qle 10^5)

    (A_{c,i})为第(i)次询问是否包含颜色(c),考虑得到其

    显然可以将(iin[l,r],p_iin[d,u])可以拆开
    (X_{i,j})为第(j)次询问是否满足(l_jle ile r_j),我们对于((l_j,j)(r_j+1,j))进行扫描线,能很轻松的得到
    (Y_{i,j})为第(j)次询问是否满足(d_jle ile u_j),同理可得
    那么((X_iAnd Y_j)_j)即为点(i)是否被包含在第(j)次询问中
    现在可以很简单得到(A)

    矩阵(A)显然为01矩阵,需要将各行按列相加,得到答案
    对于每列,这里我们如此维护其答案:

    (cnt_i)为第(i)目前的答案
    (cnt_i=sumlimits_{k=0}^{infty} 2^kcdot C_{k,i})(其中(C)为01矩阵)
    假设这里(cnt_ile N),那么改写成
    (cnt_i=sumlimits_{k=0}^{logN} 2^kcdot C_{k,i})
    (logN)为矩阵的行数,称为(h)

    那么我们分治,对于颜色(in[l,mid]),令其矩阵为(C_0),对于颜色(in(mid,r]),令其矩阵为(C_1)
    考虑合并(C_0,C_1)(C'=C_0+C_1),假设(C_0,C_1)(h_0,h_1)均为(l),为了方便,则矩阵(C)(h=l+1)
    合并的过程,相当于是二进制高精,考虑进位,很容易实现

    分析其时间复杂度
    令颜色种类为(N),令(K)为将矩阵(C)的行压成的块数
    (T(N)=2T(N/2)+KlogN)(K)为常数,提取出来
    (T(N)=2T(N/2)+logN),运用主定理得到(T(N)=N)
    时间复杂度为(O(Kcdot N))

  • 相关阅读:
    PHP 开发 APP 接口 学习笔记与总结
    Java实现 LeetCode 43 字符串相乘
    Java实现 LeetCode 43 字符串相乘
    Java实现 LeetCode 43 字符串相乘
    Java实现 LeetCode 42 接雨水
    Java实现 LeetCode 42 接雨水
    Java实现 LeetCode 42 接雨水
    Java实现 LeetCode 41 缺失的第一个正数
    Java实现 LeetCode 41 缺失的第一个正数
    Java实现 LeetCode 41 缺失的第一个正数
  • 原文地址:https://www.cnblogs.com/Grice/p/14291379.html
Copyright © 2011-2022 走看看