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个数中选到众数。
统计出现次数可以用一个技巧。开n个vector,存储1-n每个数字出现的位置,然后用二分查找区间l和r在vector中的位置,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
",max(res[i],1));
}
随机化:这里需要用mt19937,rand范围太小,会影响随机化的效果。
#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
",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
",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
",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
%d
",res);
else printf("0
");
}
717D
题意
给一个数组,每次查询一个区间,要求把该区间分成若干连续子区间,使得每个子区间内的数互质。求最少需要分成多少连续子区间。
思路
对于每个询问l-r,从最左边从左往右贪心的每次选最大的互质区间。但是多次询问肯定会超时。所以需要解决两个问题
- 计算互质区间问题,如果每个询问都从头开始计算两两互质,那么肯定会超时。所以对每个数a[i],预处理出该数所能跳转到的下一个数a[j],使得[i,j-1]区间内所有数互质。从后往前扫,对每个数分解质因数,记录每个质因数最后出现的位置p,并且把a[i]的跳转位置设为它所有质因数的最后位置的最小值。这样就处理出来了跳转表。
- 快速查询问题。一个区间内可能有很多跳转,如果按顺序去跳转那么可能会超时。很容易想到,上一步处理出来的结果符合倍增的特点。所以跑一遍倍增,每次询问就可以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
",res+1);
}
}