Codeforces Contest 1553 部分题题解

E

简要题意

给定一个长度为n的排列\(p_i\),令\(a_i=i\),询问有哪些合法的k值,使得当\(a_i\)向右移动k次后,再经过不超过m次的交换,能够变成\(p_i\)
交换指的是排列中任意两个位置中数值的交换。
举例说明向右移动:设\(a_i\)=[1,2,3,4,5,6],则\(a_i\)向右移动1次得到[6,1,2,3,4,5],\(a_i\)向右移动2次得到[5,6,1,2,3,4]。

数据范围

\(3\leq n\leq 3*10^5,0\leq m\leq \frac{n}{3}\)

简要题解

我们先考虑\(k=0\)的情况,求把{\(a_i\)}变成{\(p_i\)}的最少交换次数,据说这是一个经典问题。
我们将\(a_i\)\(p_i\)连边,会得到若干个环,那么最少交换次数等于n-环数。
现在考虑移动后的两个排列,由于\(m\leq \frac{n}{3}\),所以两个排列最多有\(\frac{2}{3}\)的位置不同,即至少有\(\frac{1}{3}\)的位置相同。
我们可以求出来,对于第\(i\)个位置,经过多少次移动才能使得两个排列在该位置相同,并且当\(k\in[0,n-1]\)时只有一个k满足要求。
因此我们容易求出,当\(a_i\)向右移动k次后,有几个位置和\(p_i\)相同。
对于合法的k,相同位置不少于三分之一,因此至多有三个k合法,对于每个k可以直接\(O(n)\)判断是否合法。
代码如下:

#include<bits/stdC++.h>
using namespace std;
const int MAXN=3e5+10;
int Ts,n,m,Ans,T[MAXN];
int May[MAXN],Out[5];
int Read()
{   int a=0,c=1;   char b=getchar();
    while(b!='-'&&(b<'0'||b>'9')) b=getchar();
    if(b=='-') c=-1,b=getchar();
    while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
    return a*c;
}
namespace SUB
{   int Ms,Fa[MAXN];
    int Find(int S){   return Fa[S]==S?S:Fa[S]=Find(Fa[S]);   }
    void Merge(int A,int B){   A=Find(A),B=Find(B),Ms+=(A!=B),Fa[A]=B;   }
    int Check(int K)
    {   Ms=0;
        for(int i=0;i<n;i++) Fa[i]=i;
        for(int i=1;i<=n;i++) Merge((i-1-K+n)%n,T[i]);
        return Ms;
    }
}using SUB::Check;
int main()
{   for(Ts=Read();Ts;Ts--)
    {   n=Read(),m=Read();
        for(int i=0;i<n;i++) May[i]=0;
        for(int i=1;i<=n;i++) T[i]=Read()-1,May[(i-1-T[i]+n)%n]++;
        for(int i=0;i<n;i++)
            if(May[i]>=n-2*m) Check(i)<=m?Out[++Ans]=i:0;
        printf("%d ",Ans);
        for(int i=1;i<=Ans;i++) printf("%d ",Out[i]);
        Ans=0,puts("");
    }
}

F

简要题意

给定一个正整数n,和一个长度为n的序列\({a_i}\),求序列\({p_i}\),其中$$p_k=\sum_{1\leq i,j\leq k}a_i\%a_j$$

数据范围

\(2\leq n\leq2*10^5,1\leq a_i\leq 3*10^5\),保证\(a_i\)互不相等

简要题解

取模操作是无法维护的,我们需要将\(a_i\%a_j\)改写成$$a_i-a_i*\lfloor\frac{a_i}{a_j}\rfloor$$

不难想到,\(p_k\)的值可以由\(p_{k-1}\)转移过来,那么\(p_k=p_{k-1}+\sum_{i=1}^{k-1}a_i\%a_k+\sum_{i=1}^{k-1}a_k\%a_i\)
不妨分开考虑,将\(p_k\)拆成两部分,令\(s_k=s_{k-1}+\sum_{i=1}^{k-1}(a_k\%a_i)\)\(t_k=t_{k-1}+\sum_{i=1}^{k-1}(a_i\%a_k)\)
那么\(p_i=s_i+t_i\),考虑如何求这两个数列。
把取模操作进行替换,有\(s_k=s_{k-1}+\sum_{i=1}^{k-1}(a_k-a_i*\lfloor \frac{a_k}{a_i}\rfloor)=s_{k-1}+a_k*(k-1)-\sum_{i=1}^{k-1}(a_i*\lfloor \frac{a_k}{a_i}\rfloor)\)
后面那个求和式,我们可以利用区间加法来维护:对于当前的\(a_i\),考虑它对后面的影响。将整个值域按照\(\lfloor\frac{x}{a_i}\rfloor\)的值可以分成若干块,\(a_i\)对每一块内的\(a_k\)产生相同的贡献,那么这里就对每一块进行区间加法操作,这样的话那个求和式只需要一个单点查询就可以解决。
时间复杂度分析,设值域上限\(M=3*10^5\),我们知道\(a_i\)互不相等,那么最多执行\(\sum_{i=1}^n\frac{M}{i}\)次区间加法,这个是调和级数,可以认为是\(Mlogn\)级别的,每次操作复杂度为\(logM\),总复杂度约为\(M*logM*logn\),可以接受。
类似地,我们可以得到\(t_k=t_{k-1}+\sum_{i=1}^{k-1}a_i-\sum_{i=1}^{k-1}a_k*\lfloor\frac{a_i}{a_k}\rfloor\)
后面的求和式可以通过区间查询来获得,即枚举\(\lfloor\frac{a_i}{a_k}\rfloor\)的值,查询这个区间内有多少个点。修改是单点修改,表示\(A[k]\)这个地方又多了一个点。
查询和修改操作都可以用树状数组实现。
代码如下:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+10;
const int MAXZ=3e5+10;
int n,Top=3e5;
ll A[MAXN],S[MAXN],T[MAXN];
int Read()
{   int a=0,c=1;   char b=getchar();
    while(b!='-'&&(b<'0'||b>'9')) b=getchar();
    if(b=='-') c=-1,b=getchar();
    while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
    return a*c;
}
int Min(int A,int B){   return A<B?A:B;   }
namespace Case
{   ll Tr[MAXZ],Pre[MAXN];
    int Lowbit(int K){   return K&(-K);   }
    void Add(int Np,ll Num){   for(Np++;Np<=Top+1;Np+=Lowbit(Np)) Tr[Np]+=Num;   }
    ll Query(int Np)
    {   ll Ret=0;
        for(Np++;Np;Np-=Lowbit(Np)) Ret+=Tr[Np];
        return Ret;
    }
    void Solve_S()
    {   for(int i=1;i<=n;i++)
        {   S[i]=S[i-1]+1ll*A[i]*(i-1)-Query(A[i]);
            for(int j=0,Le,Ri;j<=Top/A[i];j++)
                Le=j*A[i],Ri=Min(Top,(j+1)*A[i]-1),Add(Le,j*A[i]),Add(Ri+1,-j*A[i]);
        }
    }
    void Solve_T()
    {   for(int i=0;i<=Top+1;i++) Tr[i]=0;
        for(int i=1;i<=n;i++)
        {   T[i]=T[i-1]+Pre[i-1],Pre[i]=Pre[i-1]+A[i];
            for(int j=0,Le,Ri;j<=Top/A[i];j++)
                Le=j*A[i],Ri=Min(Top,(j+1)*A[i]-1),T[i]-=(Query(Ri)-Query(Le-1))*j*A[i];
            Add(A[i],1);
        }
    }
}using Case::Solve_S;using Case::Solve_T;
int main()
{   n=Read();
    for(int i=1;i<=n;i++) A[i]=Read();
    Solve_S(),Solve_T();
    for(int i=1;i<=n;i++) printf("%lld ",S[i]+T[i]);
}

G.Common Divisor Graph

题目链接

Common Divisor Graph

简要题解

观察题目,我们可以发现一些性质。
对于一次操作来说,我们其实并不需要建出\(a_{n+1}\),这步操作实际上是把\(a_i\)\(a_i+1\)连通。
那么可以肯定的是,答案一定不超过\(2\),因为我们能够通过至多两次操作,将\(a_s\)\(a_t\)都与\(2\)连通。
所以我们只需要判断答案是否可以为\(0\),以及是否可以为\(1\)

我们预处理出\(10^6\)以内的所有质数,以及每个数的最小质因子,然后对每个\(a_i\)依据题意连边,再遍历一遍判断连通性。
如果答案为\(0\),那么在原图中两个\(a_s\)\(a_t\)就在同一连通块内,可以直接判断。
如果答案为\(1\),那么我们肯定可以通过一次操作,将\(a_s\)\(a_t\)所在连通块合并。

假设我们对\(a_i\)进行了一次操作,那么我们会把\(a_i\)所在连通块和\(a_i+1\)的所有质因子所在连通块合并。
由于\(2*3*5*7*11*13*17=510510\),那么\(a_i+1\)至多有\(7\)个质因子。
但是\(10^6\)以内,只有这一个数存在\(7\)种质因子,由于\(a_i\)互不相同,实际上均摊下来,每个\(a_i+1\)的质因子种类很少。
于是我们可以对于每个\(a_i\),求出\(a_i+1\)的质因子种类,然后暴力枚举所有两个连通块合并的情况,然后扔进\(Map\)中。
处理询问时,先判断答案是否为\(0\)。若不为\(0\),则在\(Map\)中查询,是否能够直接合并\(a_s\)\(a_t\)所在连通块。

代码如下:

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
using namespace std;
const int MAXN=1e6+10;
bool Notpri[MAXN];
int n,Qs,Ps,Seq[MAXN/6],Fa[MAXN],Pri[MAXN],Minp[MAXN];
vector<int>Edge[MAXN];
map<ll,bool>Map;
int Read()
{   int a=0,c=1;   char b=getchar();
    while(b!='-'&&(b<'0'||b>'9')) b=getchar();
    if(b=='-') c=-1,b=getchar();
    while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
    return a*c;
}
int Find(int S){   return Fa[S]==S?S:Fa[S]=Find(Fa[S]);   }
void Merge(int A,int B){   A=Find(A),B=Find(B),Fa[A]=B;   }
void Dfs(int Now)
{   for(int v:Edge[Now])
        if(Find(v)!=Find(Now)) Merge(v,Now),Dfs(v);
}
void Prepare()
{   for(int i=2;i<=1000001;i++)
    {   if(!Notpri[i]) Pri[++Ps]=i,Minp[i]=i,Fa[i]=i;
        for(int j=1;j<=Ps&&Pri[j]*i<=1000001;j++)
            Notpri[i*Pri[j]]=1,Minp[i*Pri[j]]=Pri[j],i%Pri[j]?0:j=Ps;
    }
}
void Link(int Num)
{   for(int Td=Num,Np;Td>1;)
    {   Np=Minp[Td];
        if(Num!=Np) Edge[Num].pb(Np),Edge[Np].pb(Num),Fa[Num]=Num,Fa[Np]=Np;
        while(Td%Np==0) Td/=Np;
    }
}
void New(int A,int B){   A<B?Map[1ll*A*MAXN+B]=1:Map[1ll*B*MAXN+A]=1;   }
void Add(int A,int B)
{   int Fac[10],Fs=0;
    for(int Td=B,Np;Td>1;)
    {   Np=Minp[Td],Fac[++Fs]=Np;
        while(Td%Np==0) Td/=Np;
    }
    Fac[++Fs]=A;
    for(int i=1;i<=Fs;i++)
        for(int j=i+1;j<=Fs;j++) New(Find(Fac[i]),Find(Fac[j]));
}
bool Check(int A,int B){   return A<B?Map.find(1ll*A*MAXN+B)!=Map.end():Map.find(1ll*B*MAXN+A)!=Map.end();   }
int main()
{   n=Read(),Qs=Read(),Prepare();
    for(int i=1;i<=n;i++) Seq[i]=Read(),Fa[Seq[i]]=Seq[i],Link(Seq[i]);
    for(int i=1;i<=n;i++) Dfs(Seq[i]);
    for(int i=1;i<=n;i++) Add(Find(Seq[i]),Seq[i]+1);
    for(int i=1,A,B;i<=Qs;i++)
        A=Find(Seq[Read()]),B=Find(Seq[Read()]),printf("%d\n",A==B?0:Check(A,B)?1:2);
}

H.XOR and Distance

题目链接

XOR and Distance

简要题解

对于含有位运算的题,通常需要按位考虑贡献。
我们需要对于每一个\(x\),求出\(a_i\bigoplus x\)的序列,然后求这个序列的最大值与最小值的差,显然不能每次暴力算出所有的\(a_i\bigoplus x\)
事实上,如果两个数\(x_1\)\(x_2\)在二进制表示下差别很小,那么对应的\(a_i\bigoplus x_1\)\(a_i\bigoplus x_2\)的差别也很小,通过转移应该可以重复利用信息。
既然要转移,那么就得有状态,可以直接按照题目里面来做,我们设\(F[x]\)表示序列\(a_i\bigoplus x\)中最大值与最小值的差。
如果\(F[x]\)一次性维护所有的\(a_i\),会很难,于是我们可以先维护少数几个\(a_i\),再慢慢通过转移将所有的\(a_i\)算入贡献。

具体地说,我们需要维护三个数组:\(F[x],Minv[x],Maxv[x]\)
其中\(Minv[x]\)表示当前已经计算过贡献的\(a_i\)中,最小的\(a_i\bigoplus x\)是多少,而\(Maxv[x]\)表示的是最大的\(a_i\bigoplus x\)
初始状态,\(F[x],Minv[x],Maxv[x]\)都只维护\(x\)这一个值域,由于\(a_i\)互不相同,这个值域内至多有一个\(a_i\),故\(F[x]\)不存在方案,设为\(Inf\)
如果存在\(a_i=x\),那么\(Minv[x]\)\(Maxv[x]\)设为\(0\),否则为了方便转移,各自设为\(Inf\)\(-Inf\)

我们从低到高枚举位数,设最低位为第\(0\)位,假设当前枚举到了第\(b\)位,那么三个数组已经维护好的值域是\([x>>b<<b,x|(2^b-1)]\)
这个值域的含义自己想想应该能明白,现在我们现在想将维护的值域扩大一倍,自然是合并左右两个值域。
我们枚举维护了右值域信息的\(Ri\),那么\(Ri\)的第\(b\)位必须为\(1\)\(Le=Ri\bigoplus 2^b\)就维护了左值域的信息。
如何合并\(F[Le]\)\(F[Ri]\)?我们要选出\(a_i\)\(a_j\)作差,这个时候可以分情况讨论一下。
如果\(a_i\)\(a_j\)在同一边值域内,那么它们的第\(b\)位相等,而\(Le\)\(Ri\)只有第\(b\)位不同,因此没有影响,得到转移:$$F[Le]=F[Ri]=Min(F[Le],F[Ri])$$
如果\(a_i\)\(a_j\)在不同边值域内,对于\(F[Le]\)来说,在\(Ri\)值域内的\(a_i\)异或上\(Le\)之后,第\(b\)位一定为\(1\),相应的在\(Le\)值域内的第\(b\)位一定是\(0\)
那么右边所有的\(a_i\bigoplus Le\)都会大于左边所有的\(a_i\bigoplus Le\),为了使差最小,自然要选右边最小的和左边最大的。
我们单独处理一下第\(b\)位,于是得到转移:$$F[Le]=Min(F[Le],Minv[Ri]+2^b-Maxv[Le])$$
对于\(F[Ri]\)的分析也是一个道理,得到:

\[F[Ri]=Min(F[Ri],Minv[Le]+2^b-Maxv[Ri]) \]

还要继续维护\(Minv[x]\)\(Maxv[x]\)的值。左右两边值域内的\(a_i\),分别异或\(Le\)\(Ri\),只有第\(b\)位不一样,单独处理一下就行。
按顺序把所有位全部处理完,\(F[x]\)就得到了整个值域内的信息,也就考虑了所有的\(a_i\),成了我们想要的答案。
时间复杂度\(O(k*2^k)\)
代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1<<19|7;
const int Inf=1e9;
int n,K,Top,A[MAXN];
int F[MAXN],Minv[MAXN],Maxv[MAXN];
int Read()
{   int a=0,c=1;   char b=getchar();
    while(b!='-'&&(b<'0'||b>'9')) b=getchar();
    if(b=='-') c=-1,b=getchar();
    while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
    return a*c;
}
int Min(int A,int B){   return A<B?A:B;   }
int Max(int A,int B){   return A>B?A:B;   }
int main()
{   n=Read(),K=Read(),Top=(1<<K)-1;
    for(int i=0;i<=Top;i++) F[i]=Minv[i]=Inf,Maxv[i]=-Inf;
    for(int i=1;i<=n;i++) A[i]=Read(),Minv[A[i]]=Maxv[A[i]]=0;
    for(int Nb=0,Bit=1;Nb<K;Nb++,Bit<<=1)
        for(int Ri=0,Le;Ri<=Top;Ri++)
        {   if(~Ri&Bit) continue ;
            Le=Ri^Bit,F[Le]=F[Ri]=Min(F[Le],F[Ri]);
            int Minr=Minv[Ri],Maxr=Maxv[Ri],Minl=Minv[Le],Maxl=Maxv[Le];
            F[Le]=Min(F[Le],Minr+Bit-Maxl),F[Ri]=Min(F[Ri],Minl+Bit-Maxr);
            Minv[Le]=Min(Minl,Minr+Bit),Maxv[Le]=Max(Maxl,Maxr+Bit);
            Minv[Ri]=Min(Minr,Minl+Bit),Maxv[Ri]=Max(Maxr,Maxl+Bit);
        }
    for(int i=0;i<=Top;i++) printf("%d ",F[i]);
}
posted @ 2021-07-23 14:15  Alkaid~  阅读(69)  评论(0编辑  收藏  举报