Codeforces Round #716&#717 (Div. 2)

716D

题意

给一个长为n的数列a。对于一个区间的元素,可以把它分为若干个子序列(可以不连续的分)。现在有q次询问,每次询问求一个区间l-r内最少要把区间分成多少个子序列,使得每个子序列出现最多次的元素不超过区间长度的一半(向上取整)。

思路

首先很容易想到,一个子序列只能有一个元素出现次数超过区间长度一半,所以只需要考虑子序列的众数就可以了。

然后再考虑划分的问题。

假设现在众数出现k次,那么它需要k-1个不等与它的数分配在一起,才能使得这个子序列合法。

对于某个区间,可以把里面的元素分成两类,众数和非众数。假设众数出现k次,那么如果非众数的数量大于等于k-1,则序列合法。否则需要划分。

因为出现k次的众数需要k-1个非众数,难么把k划分为a+b,则需要a-1+b-1个非众数。即k-2个非众数。以此类推,可以得出规律,把区间划分为n个合法子序列,最少需要k-n个非众数。

又由非众数的数量为len-k,可得k-n=len-k,即n=2k-len。所以问题就转化成了无修改多次询问区间众数的出现次数。

区间众数问题,有很多解法,本题大概有5种解法。

1.首先很容易想到用莫队算法。不过莫队是离线的,要求强制在线的题目无法使用。本题不强制在线,故可以使用。时间复杂度O(nsqrt(n))

2.除此之外,应当注意到,只有超过区间长度一半的众数才会对答案有贡献,所以如果有这样的众数,随机在区间内选一个数,选到该众数的可能性大于1/2

所以考虑随机化。每次在区间内随便选m个数,那么这些数都不是众数的概率为1/2^m。所以取一个合适的m,就可以大概率在m个数中选到众数。

统计出现次数可以用一个技巧。开nvector,存储1-n每个数字出现的位置,然后用二分查找区间lrvector中的位置,upper_bound(r)-lower_bound(l)就是出现次数,然后每次取最大值,最后得到答案。假设取m个随机数,总体复杂度为O(qmlog(n))

3.还可以用线段树解。不妨称出现次数超过区间长度的数为超众数。虽然众数问题不能分治,但是超众数可以分治解决,因为不存在某个不是左右区间超众数的数成为大区间的超众数,所以合并只需要考虑左右区间的超众数。

线段树维护左右区间合并时只需要考虑超众数相等的值,以及左右区间一个存在一个不存在的情况,因为左右区间超众数值不相等则一定不会使得大区间有超众数。

如果左右区间一个有超众数一个不存在,那么用二分的方法统计存在的那个超众数在大区间内的出现次数,就可以保持区间的性质,使器可以向上合并。

查询时只需要每次二分查找每个在l-r范围内的子区间内的超众数在l-r区间内的出现次数即可。总体复杂度O(qlog^2(n))

4.数列分块。可以参考clj大佬的《区间众数解题报告》。可以O(nsqrt(n))在线解决。但是代码比较复杂,而且常数比较大,用来解决本题优先级不是很高。

5.可持久化线段树。对区间内数的个数建树,某节点存储有多少数据出现在该节点代表的区间内。(如某节点代表1-2区间,则存储有多少[1,2]之间的数,具体可以参考可持久化线段树相关教程)。建树完成之后,对于每次询问l,r,利用前缀和性质,查询版本r到版本l-1之间是否有出现次数超过(r-l+2)/2的数,有则返回出现次数。然后计算答案,总体复杂度O(nlog(n)),速度最快但是空间复杂度较大。

代码

莫队:

#include<bits/stdc++.h>
using namespace std;
 
const int MAX=3e5+5;
 
int bl[MAX],a[MAX],cnt[MAX],num[MAX],res[MAX],cur;
 
struct Query
{
    int l,r,id;
    bool operator <(const Query A)
    {
        if(bl[l]==bl[A.l])return bl[r]<bl[A.r];
        return bl[l]<bl[A.l];
    }
}q[MAX];
 
void del(int x)
{
    num[cnt[a[x]]]--;
    num[--cnt[a[x]]]++;
    while(!num[cur])cur--;
}
 
void add(int x)
{
    num[cnt[a[x]]]--;
    num[++cnt[a[x]]]++;
    while(num[cur+1])cur++;
}
 
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    int root=sqrt(n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),bl[i]=(i-1)/root+1;
    for(int i=0;i<m;i++)
        scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
    sort(q,q+m);
    cur=0,num[0]=1;
    int l=1,r=0;
    for(int i=0;i<m;i++)
    {
        while(l>q[i].l)add(--l);
        while(r<q[i].r)add(++r);
        while(l<q[i].l)del(l++);
        while(r>q[i].r)del(r--);
        res[q[i].id]=cur*2-q[i].r+q[i].l-1;
    }
    for(int i=0;i<m;i++)
        printf("%d\n",max(res[i],1));
}

随机化:这里需要用mt19937rand范围太小,会影响随机化的效果。

#include<bits/stdc++.h>
using namespace std;
 
const int MAX=3e5+5;
 
int a[MAX];
vector<int>pos[MAX];
 
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),pos[a[i]].push_back(i);
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    for(int i=0;i<m;i++)
    {
        int T=30,l,r,maxx=-1;
        scanf("%d%d",&l,&r);
        uniform_int_distribution<int>rn(l,r);
        while(T--)
        {
            int cur=a[rn(rng)];
            maxx=max(maxx,upper_bound(pos[cur].begin(),pos[cur].end(),r)-lower_bound(pos[cur].begin(),pos[cur].end(),l));
        }
        printf("%d\n",max(1,2*maxx-r+l-1));
    }
}

线段树:

#include<bits/stdc++.h>
using namespace std;
 
const int MAX=3e5+5;
 
struct data
{
    int maxx;
}tree[MAX<<2];
int a[MAX];
vector<int>pos[MAX];
 
int cnt(int l,int r,int key)
{
    return upper_bound(pos[key].begin(),pos[key].end(),r)-lower_bound(pos[key].begin(),pos[key].end(),l);
}
 
void pushup(int rt,int l,int r)
{
    int ls=rt<<1,rs=rt<<1|1;
    if(tree[ls].maxx==tree[rs].maxx)tree[rt].maxx=tree[ls].maxx;
    else tree[rt].maxx=(cnt(l,r,tree[ls].maxx)>cnt(l,r,tree[rs].maxx))?tree[ls].maxx:tree[rs].maxx;
}
 
void build(int l,int r,int rt)
{
    if(l==r)
    {
        tree[rt].maxx=a[l];
        return ;
    }
    int m=(l+r)>>1;
    build(l,m,rt<<1);
    build(m+1,r,rt<<1|1);
    pushup(rt,l,r);
}
 
int query(int L,int R,int l,int r,int rt)
{
    if(L<=l&&r<=R)
    {
        return cnt(L,R,tree[rt].maxx);
    }
    int m=(l+r)>>1,ans=-1;
    if(L<=m)
        ans=max(ans,query(L,R,l,m,rt<<1));
    if(R>m)
        ans=max(ans,query(L,R,m+1,r,rt<<1|1));
    return ans;
}
 
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),pos[a[i]].push_back(i);
    build(1,n,1);
    while(m--)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        int maxx=query(l,r,1,n,1);
        printf("%d\n",max(1,maxx*2-r+l-1));
    }
}

可持久化线段树:

#include<bits/stdc++.h>
using namespace std;
const int MAX=3e5+5;
 
struct Node
{
    int cnt,ls,rs;
} tree[MAX<<5];
int rt[MAX],tot=0;
 
int build(int l,int r)
{
    int cur=++tot;
    if(l==r)return cur;
    int mid=(l+r)>>1;
    tree[cur].ls=build(l,mid);
    tree[cur].rs=build(mid+1,r);
    return cur;
}
 
int update(int l,int r,int pre,int pos)
{
    int cur=++tot;
    tree[cur].ls=tree[pre].ls;
    tree[cur].rs=tree[pre].rs;
    tree[cur].cnt=tree[pre].cnt+1;
    if(l==r)return cur;
    int mid=(l+r)>>1;
    if(pos<=mid)
        tree[cur].ls=update(l,mid,tree[pre].ls,pos);
    else
        tree[cur].rs=update(mid+1,r,tree[pre].rs,pos);
    return cur;
}
 
int query(int x,int y,int l,int r,int k)
{
    if(l==r)
        if(tree[y].cnt-tree[x].cnt>k)return tree[y].cnt-tree[x].cnt;
    int mid=(l+r)>>1;
    if(tree[tree[y].ls].cnt-tree[tree[x].ls].cnt>k)return query(tree[x].ls,tree[y].ls,l,mid,k);
    if(tree[tree[y].rs].cnt-tree[tree[x].rs].cnt>k)return query(tree[x].rs,tree[y].rs,mid+1,r,k);
    return 0;
}
 
int main()
{
    int n,m,x;
    scanf("%d%d",&n,&m);
    rt[0]=build(1,n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&x);
        rt[i]=update(1,n,rt[i-1],x);
    }
    while(m--)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        int maxx=query(rt[l-1],rt[r],1,n,(r-l+2)/2);
        printf("%d\n",max(1,maxx*2-r+l-1));
    }
}

717C

题意

n个数,要求删除若干个数,使得这些数无法分为两个和相等的集合。求删除哪些数。

思路

显然可以想到,如果n个数的和sum为奇数,那么一定不存在两个集合和相等。如果sum为偶数,那么某一集合的和一定为sum/2

如果sum为偶数,那么一定可以删除某一个奇数,使得sum为奇数,自然满足条件。

如果不存在某个数为奇数,那么可以把每个数除2,直到其中某一个数变为奇数。因为如果n个数不存在某种划分方式使得两集合和相等,那么将这些数都乘2,也不会存在某种划分方式使得两集合和相等,因为数乘2后是一一对应的。

还有一种情况,即sum为偶数,但是并不存在某种划分方式(很容易可以想到这种情况的例子)。所以还需要判断是否存在划分方式。

可以等价为是否存在某个子集,使得子集和为sum/2,可以抽象成背包问题。

代码

学习了一下用bitset写背包,很高效,但是只能用在判断方案是否存在的问题。

#include<bits/stdc++.h>
using namespace std;
 
const int MAX=1e2+5;
int a[MAX];
 
int test(int sum,int n)
{
    if(sum&1) return -1;
    bitset<MAX*2000>dp;
    dp[0]=1;//dp[i]表示是否存在重量为i的方案。
    int minn=MAX,res=-1;
    for(int i=0;i<n;i++)
    {
        int k=0;
        while(!(a[i]&(1<<k)))k++;
        if(k<minn){minn=k;res=i+1;}
        dp|=(dp<<a[i]);//转移,将前一种状态左移,然后按位或
    }
    if(dp[sum/2])return res;
    else return -1;
}
 
int main()
{
    int n,sum=0,res=-1;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
        sum+=a[i];
    }
    res=test(sum,n);
    if(res>0) printf("1\n%d\n",res);
    else printf("0\n");
}

717D

题意

给一个数组,每次查询一个区间,要求把该区间分成若干连续子区间,使得每个子区间内的数互质。求最少需要分成多少连续子区间。

思路

对于每个询问l-r,从最左边从左往右贪心的每次选最大的互质区间。但是多次询问肯定会超时。所以需要解决两个问题

  1. 计算互质区间问题,如果每个询问都从头开始计算两两互质,那么肯定会超时。所以对每个数a[i],预处理出该数所能跳转到的下一个数a[j],使得[i,j-1]区间内所有数互质。从后往前扫,对每个数分解质因数,记录每个质因数最后出现的位置p,并且把a[i]的跳转位置设为它所有质因数的最后位置的最小值。这样就处理出来了跳转表。
  2. 快速查询问题。一个区间内可能有很多跳转,如果按顺序去跳转那么可能会超时。很容易想到,上一步处理出来的结果符合倍增的特点。所以跑一遍倍增,每次询问就可以log(n)完成。

代码

#include<bits/stdc++.h>
using namespace std;
 
const int MAX=1e5+5;
const int INF=0x3f3f3f3f;
int last_place[MAX],a[MAX],dp[MAX][20];
 
int main()
{
    int n,q;
    scanf("%d%d",&n,&q);
    for(int i=0; i<n; i++)
        scanf("%d",&a[i]);
    int maxx=n;
    memset(last_place,INF,sizeof last_place);
    for(int i=n-1; i>=0; i--)
    {
        for(int j=2; j*j<=a[i]; j++)
        {
            if(a[i]%j==0)
            {
                maxx=min(maxx,last_place[j]);
                last_place[j]=i;
                while(a[i]%j==0)a[i]/=j;
            }
        }
        if(a[i]!=1)
        {
            maxx=min(maxx,last_place[a[i]]);
            last_place[a[i]]=i;
        }
        dp[i][0]=maxx;
    }
    for(int i=0;i<20;i++)dp[n][i]=n;
    for(int i=n-1; i>=0; i--)
        for(int j=1; j<20; j++)
            dp[i][j]=i+(1<<j)>=n?n:dp[dp[i][j-1]][j-1];
    while(q--)
    {
        int l,r,res=0;
        scanf("%d%d",&l,&r);
        l--;
        r--;
        for(int i=19; i>=0; i--)
            if(dp[l][i]<=r)
            {
                l=dp[l][i];
                res+=(1<<i);
            }
        printf("%d\n",res+1);
    }
}
posted @ 2021-06-04 12:13  cryingrain  阅读(77)  评论(0编辑  收藏  举报