zoukankan      html  css  js  c++  java
  • smoj2828子数组有主元素

    题面

    一个数组B,如果有其中一个元素出现的次数大于length(B) div 2,那么该元素就是数组B的主元素,显然数组B最多只有1个主元素,因为数组B有主元素,所以被称为“优美的”。
    
    给出数组A[0..n-1],问数组A有多少个“优美的”子数组。数组A的子数组是由数组A的连续若干个元素构成的数组。数组A不是直接给出的,而是通过如下公式自动产生的:
    
    for i = 0 to n-1 do
    
    {
    
      A[i] = (seed div 2^16) % m
    
      seed = (seed * 1103515245 + 12345) % 2^31
    
    }
    
    如上公式中:  n, seed, m都是输入数据给出的,div是表示整数的整除。^是表示幂运算。
    
    输入格式:
    一行,3个整数,n,  seed,  m。1 <= n <= 100000。 0 <= seed <= 2^31-1。 1 <= m <= 50。
    
    输出格式
    一个整数。
    

    样例

    输入样例1
    5  200  5
    输出样例1
    8
    
    输入样例2
    10  15  3
    输出样例2
    23
    
    输入样例3
    8  12345678  1
    输出样例3
    36
    
    输入样例4
    27  541  50
    输出样例4
    27
    

    1s,256MB

    思路

    由题目可以发现主元素在每一个子数组里只有一个。并且m很小,这说明了生成出的A[i]最大值只有m-1那么大。

    m最多只有50,所以我们可以枚举每一个元素作为主元素在A数组中出现了多少次,然后最后累加一下答案就ok了。

    那么现在问题来的,如何计算一个元素为主元素在A数组用出现了多少次。

    不妨设钦定的主元素是x

    我们简单地做一个差分,把a[i]==x的位置都设为1,不然就设为-1,然后做出来一个差分数组。

    不如举个例子:

    a[i]={0,0,1,2,0}
    钦定主元素:x=0
    

    那么做出的差分数组:

          1 1 -1 -1 1
    b[i]={1,2,1,0,1}
    

    那么这个差分数组是什么意思呢?不难发现,如果我们要查询区间[1,3]中的主元素是不是x。将b[3]-b[1-1]就得出了1,这个1的意思是区间[1,3]中(x的个数)与(不是x的个数)的差。明显,如果这个差大于0,就说明这个区间是主元素为x的区间。

    那么我们知道这个差分数组的特性了,可以思考,我们枚举这个区间的开头i,那么可不可以快速算出有多少B数组的值与b[i-1]的大于0。

    这时候就有了一个办法:我们维护一个可以支持查询kth的数据结构,然后每次直接区间查询[i+1,n]大于b[i-1]的数有多少。时间复杂度为O(nmlog(n)),而且特别难写。那么有没有什么办法可以转换成简单一点的呢?

    第一层转换

    前面说了,我们要查询大于b[i-1]的数有多少,那么我们可以把B数组的值域都记下来,为vis数组,那么上面样例的vis数组为

    vis[0]=1;
    vis[1]=3;
    vis[2]=1;
    

    同时做一个后缀和 sum数组:

    sum[0]=5;
    sum[1]=4;
    sum[2]=1;
    

    不难发现其实我们查询的大于b[i-1]的数有多少就是,sum[b[i-1]+1]。就不需要查询kth了。

    但还有个问题,我们查询完sum数组,开头i往下一个跳,区间就少了一个b[i],vis[b[i]]要减1,自然,sum数组从开头到b[i]都要减1。

    即当i=2时,vis数组应该是这样的

    vis[0]=1;
    vis[1]=2;
    vis[2]=1;
    

    sum数组应该是这样的

    sum[0]=4;
    sum[1]=3;
    sum[2]=1;
    

    这个问题很好解决,我们可以开一个树状数组来维护,每次查询(b[i-1]+1)的值,然后将1至b[i]都减1。

    初始化树状数组tree[i]=sum[i]

    时间复杂度:O(nmlog(n)),但代码好些了很多。下面会将第二层优化,时间复杂度将优化成O(nm),是在这个方法的基础上优化的,希望大家先理解这个方法。

    代码实现的一些说明:

    1、首先,我们写的时候要加上偏移值,因为vis数组下标有可能是负数
    
    2、我实现中的zz的意思是一个指针,指向b[i-1]+1的值,因为我们发现每一次开头i的变化,b[i-1]到b[i]只有可能加1或减1,因为差分数组的特殊性。所以这个(b[i-1]+1)我就用了一个指针来维护。
    
    例如:刚开始b[1-1]为0,zz指向0,a[1]为x,那么b[2-1]就会为1,那么zz就加1,指向1。如果a[2]不为x,那么b[3-1]就会为0,那么zz就减1,指向0。
    

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define int long long
    #define maxn 400001
    const int pyz=100501;
    int a[maxn],vis[maxn],b[maxn],sum[maxn],ans,tree[maxn];
    int n,SEED,m,Ans;
    inline int lowbit(int x)
    {return x&-x;}
    inline void generate(int n,int SEED,int m){		//生成A数组
    	for(int i=1;i<=n;i++){
    		a[i]=(SEED/65536ll)%m;
    		SEED=(SEED*1103515245ll+12345ll)%2147483648ll;
    	}
    }
    inline void add(int p,int val){
        while(p<maxn){
            tree[p]+=val;
            p+=lowbit(p);
        }
    }
    inline void add_(int x,int y,int sum){
        add(x,+sum);
        add(y+1,-sum);
    }
    inline int sum_(int p){
        ans=0;
        while(p!=0){
            ans+=tree[p];
            p-=lowbit(p);
        }
        return ans;
    }
    inline int solve(int x){
    	b[0]=0;long long ans=0,zz=pyz,begin=0,t,sum=0;	//记得要加上偏移值,负数数组存不了
    	memset(tree,0,sizeof(tree));
    	memset(vis,0,sizeof(vis));
    	for(int i=1;i<=n;++i){
    		if(a[i]==x)b[i]=b[i-1]+1;	//计算b数组
    		else b[i]=b[i-1]-1;
    		vis[b[i]+pyz]++;			//计算vis数组
    	}
    	for(int i=pyz*2;i>=1;--i){
    		sum+=vis[i];				//计算sum数组,后缀和
    		add_(i,i,sum);				//同时更新树状数组tree数组
    	}
    	for(int i=1;i<=n;++i){
    		t=sum_(zz+1);				//大于所以要加1
    		if(a[i]==x)zz++;else zz--;	//更改指针的值
    		ans+=t;add_(1,zz,-1);		//更新值
    	}
    	return ans;
    }
    signed main(){
    	freopen("2828.in","r",stdin);
    	freopen("2828.out","w",stdout);
    	scanf("%lld%lld%lld",&n,&SEED,&m);
    	generate(n,SEED,m);
    	for(int i=0;i<m;i++){
    		Ans+=solve(i);
    	}
    	printf("%lld
    ",Ans);
    	return 0;
    }
    

    脸黑,常数大,别人O(nmlog(n))都过了,就我只有73分,不过这也激发了我探究O(nm)复杂度的决心。

    第二层优化 时间复杂度O(nm)

    辣么,我们现在来讲终究算法,不仅好写,时间复杂度还优。

    我们现在来分析树状数组的做法,我们发现每一次指针跳只会一个一个跳,所以实际上树状数组改的很多地方都没有用,可能根本不会查询那里,所以这就导致了时间上的浪费。

    既然指针只会一个一个跳,那么我们可以不用树状数组来维护,我们打标记tag,跳到哪,更新到哪。

    打tag的方式也很简单,修改自身值的同时,把tag传的下一个去,自身清0。

    if(tag[zz]>0)sum[zz]-=tag[zz],tag[zz-1]+=tag[zz],tag[zz]=0;
    

    是不是简单到爆炸!!!

    这样通过指针的特殊性,我们把那个log的时间复杂度给省掉了,时间复杂度降为O(nm)。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define int long long
    #define maxn 200011
    const int pyz=100001;
    int a[maxn],vis[maxn],b[maxn],sum[maxn],ans,tree[maxn],tag[maxn];
    int n,SEED,m,Ans;
    inline void generate(int n,int SEED,int m){
    	for(int i=1;i<=n;i++){
    		a[i]=(SEED/65536ll)%m;
    		SEED=(SEED*1103515245ll+12345ll)%2147483648ll;
    	}
    }
    inline int solve(int x){
    	b[0]=0;long long ans=0,zz=pyz;
    	memset(vis,0,sizeof(vis));memset(tag,0,sizeof(tag));
    	for(register int i=1;i<=n;++i){
    		if(a[i]==x)b[i]=b[i-1]+1;
    		else b[i]=b[i-1]-1;
    		vis[b[i]+pyz]++;			//求vis数组
    	}
    	for(register int i=pyz*2;i>=1;--i)sum[i]=sum[i+1]+vis[i];		//求sum数组
    	for(register int i=1;i<=n;++i){
    		if(tag[zz]>0)sum[zz]-=tag[zz],tag[zz-1]+=tag[zz],tag[zz]=0;		//打tag
    		ans+=sum[zz+1];			//计算答案
    		if(a[i]==x)zz++;else zz--;
    		sum[zz]--;tag[zz-1]++;	//更新tag,自身值
    	}
    	return ans;
    }
    signed main(){
    	freopen("2828.in","r",stdin);
    	freopen("2828.out","w",stdout);
    	scanf("%lld%lld%lld",&n,&SEED,&m);
    	generate(n,SEED,m);
    	for(int i=0;i<m;++i)Ans+=solve(i);
    	printf("%lld
    ",Ans);
    	return 0;
    }
    

    哈哈哈,O(nm)是不是很简单啊。

    你以为这是极限了吗?

    不,事情远远没有你想想那么简单

    如果m不是50了怎么办,m是10^9怎么办,会超时哦

    第三层扩展性优化,m很大也能做!

    时间复杂度O(nsqrt(n))

    咳咳,这里只是一个扩展性的做法,针对m很大的做法,当然到这题没有用,不过还是写一下。

    首先,第一步,将a[i]离散化,基础步骤

    然后,第二步,分类讨论,如果元素出现种数小于sqrt(n),那么就直接跑上面O(nm)的做法,时间复杂度O(nsqrt(n))

    接着,第三步,如果种数大于sqrt(n),那么就把出现次数小于sqrt(n)的元素为主元素的数组个数找出来,其实就是对于区间长度为[1,2sqrt(n)]的有主元素的子数组都找出来。但是如果,某个区间的主元素是元素x,这个元素x的总出现次数大于sqrt(n),那么就不加,避免与下面的计算重复。

    那么,第四步,剩下的出现次数大于sqrt(n)的元素个数肯定不超过sqrt(n)个,这个简单证明一下就可以了。然后对于这几个元素跑一遍上面说的O(nm),找子数组个数。

    最后,第五步,将全部答案加起来。

    嗯~,这就是O(n sqrt(n))的做法。

    谢谢观赏

  • 相关阅读:
    PHP递归方法实现前序、中序、后序遍历二叉树
    php循环方法实现先序、中序、后序遍历二叉树
    Mac charles 抓取https请求,安装证书后还是显示unknown
    PHP工厂模式
    PHP策略模式2
    PHP单例模式
    PHP 面试知识点整理归纳
    十大迷你iPhone天气应用
    来自极客标签10款最新设计素材-系列十三
    帮助快速生成页面固定显示元素的jQuery插件
  • 原文地址:https://www.cnblogs.com/hyfhaha/p/10678171.html
Copyright © 2011-2022 走看看