#23 CF1285F & CF1588F & CF1515I
Classical?
题目描述
解法
感觉被题目名称深深嘲讽了一波,这么经典我都不会做。
思考简化的问题,假设加上 \(\gcd (a_i,a_j)=1\) 怎么做?由于 \(\tt lcm\) 的形式都是 \(x\times y\),我们利用偏序关系来少枚举一些东西。我们先把 \(a\) 数组从大到小排序,然后依次扫描它的每一个元素。
考虑我们只需要找到最大的与它互质的数即可,但貌似这并不好找。一个关键的 \(\tt observation\) 是:设现在扫到的数为 \(x\),最大与它互质的数是 \(y\),那么对于所有 \(x<z\leq y\),这些 \(z\) 都可以不需要考虑了。
这是因为这些 \(z\) 后续产生的答案肯定不如 \(x\times y\) 优,那么我们可以暴力找这个 \(y\),用栈来维护,每次把栈顶弹掉直接找到 \(y\) 为止。复杂度是基于每个点只会入栈出栈一次,所以我们需要提前计算出栈中于 \(x\) 互质的数的个数,那么弹栈的时候就能恰到好处地停止。设 \(cnt(x)\) 表示栈中为 \(x\) 倍数的数,简单莫比乌斯反演可以得到:
那么单独做一次时间复杂度 \(O(n\log n)\),枚举 \(\gcd\) 分别做,可以做到 \(O(n\log^2n)\)
有个很巧妙的优化:考虑一定存在 \(a|x,b|y\) 使得 \(a\cdot b=lcm(x,y)\) 并且 \(\gcd(a,b)=1\),那么我们把给定数的所有因数拿出来,只需要用上面的方法做一次即可,时间复杂度 \(O(n\log n)\)
总结
对答案 \(\tt roughly\) 的筛选是很重要的,通过粗略的偏序关系可以知道一些情况下有些东西不需要考虑。
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,s[M],a[M],b[M];long long ans;
int cnt,p[M],vis[M],mu[M];vector<int> g[M];
void init(int n)
{
mu[1]=1;
for(int i=2;i<=n;i++)
{
if(!vis[i]) p[++cnt]=i,mu[i]=-1;
for(int j=1;j<=cnt && i*p[j]<=n;j++)
{
vis[i*p[j]]=1;
if(i%p[j]==0) break;
mu[i*p[j]]=-mu[i];
}
}
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j+=i)
g[j].push_back(i);
}
int gcd(int a,int b)
{
return !b?a:gcd(b,a%b);
}
signed main()
{
n=read();m=100000;init(m);
for(int i=1;i<=n;i++)
{
int x=read();a[x]=1;
ans=max(ans,1ll*x);
}
for(int i=1;i<=m;i++)
for(int j=i;j<=m;j+=i)
a[i]|=a[j];
for(int i=m;i>=1;i--)
{
if(!a[i]) continue;
int y=0;
for(int x:g[i]) y+=mu[x]*b[x];
while(y)
{
int j=s[k--];
if(gcd(i,j)==1)
ans=max(ans,1ll*i*j),y--;
for(int x:g[j]) b[x]--;
}
for(int x:g[i]) b[x]++;
s[++k]=i;
}
printf("%lld\n",ans);
}
Jumping Through the Array
题目描述
解法
考虑询问分块,每 \(\sqrt n\) 次操作分成一块处理,处理完之后暴力重构。
发现这样会产生一些很好的性质,我们可以把涉及三操作的点全部标记出来。然后对于每个标记点,我们找到其极长空白前驱段进行染色,那么具有相同颜色的点就是一个等价类,可以缩成一个点,那么场上就只剩下了 \(O(\sqrt n)\) 个点。
那么现在二三操作都可以暴力进行,考虑一操作怎么做?
对于每个等价类考虑其对询问的贡献,可以预处理出前 \(i\) 个点中有多少个点在这个等价类中(注意 \(i\) 的取值也只有 \(O(\sqrt n)\) 个),那么差分一下就可以计算了。
时间复杂度 \(O(n\sqrt n)\),可以适当调调块长。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 200005;
const int N = 1005;
#define ll long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,p[M],col[M],pre[M],vis[M],b[M];
int id[M],sz[N<<1][N<<1];ll s[M],a[M],ad[M];
struct node{int op,x,y;}q[M];
void color(int x,int c)
{
for(;col[x]==0;x=pre[x]) col[x]=c;
}
void add(int x,int c)
{
for(;!vis[x];x=col[p[x]]) ad[x]+=c,vis[x]=1;
for(;vis[x]==1;x=col[p[x]]) vis[x]=0;
}
signed main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=n;i++) pre[p[i]=read()]=i;
m=read();
for(int i=1;i<=m;i++)
q[i].op=read(),q[i].x=read(),q[i].y=read();
for(int l=1;l<=m;l+=N)
{
int r=min(l+N-1,m);k=0;
for(int i=0;i<=n;i++)
ad[i]=id[i]=col[i]=vis[i]=b[i]=0;
for(int i=1;i<=n;i++)
s[i]=s[i-1]+a[i];
//color all nodes
for(int i=l;i<=r;i++)
{
if(q[i].op==2) col[q[i].x]=q[i].x;
if(q[i].op==3)
col[q[i].x]=q[i].x,
col[q[i].y]=q[i].y;
}
for(int i=1;i<=n;i++) if(col[i]==i)
b[++k]=i,col[i]=0,color(i,i);
//process all query I
for(int i=l,t=0;i<=r;i++) if(q[i].op==1)
{
if(!id[q[i].x-1]) id[q[i].x-1]=++t;
if(!id[q[i].y]) id[q[i].y]=++t;
}
for(int i=0;i<=n;i++)
{
if(i) ad[col[i]]++;
if(id[i]) for(int j=1;j<=k;j++)
sz[id[i]][j]=ad[b[j]];
}
for(int i=0;i<=n;i++) ad[i]=0;
//brute simulate
for(int i=l;i<=r;i++)
{
int op=q[i].op,x=q[i].x,y=q[i].y;
if(op==1)
{
ll ans=s[y]-s[x-1];
for(int j=1;j<=k;j++)
ans+=ad[b[j]]*(sz[id[y]][j]
-sz[id[x-1]][j]);
printf("%lld\n",ans);
}
if(op==2) add(x,y);
if(op==3)
{
swap(p[x],p[y]);
pre[p[x]]=x;pre[p[y]]=y;
}
}
for(int i=1;i<=n;i++) a[i]+=ad[col[i]];
}
}
Phoenix and Diamonds
题目描述
解法
显然的想法是线段树二分,我们按照预处理出的顺序,每次能取则取。但是这样会遇到一个问题:当遇到重量过大的物品时,我们可能会在这里停下,所以处理次数会大大增加。
那么有一个解决这个问题的想法是,我们按照某个标准把物品分成重物品和轻物品。这样可以把重物品特殊处理或者直接跳过,轻物品大段选取,可能可以优化停下来的次数。
理想的情况是我们只停下来 \(O(\log c)\) 次,那么我们直接套用二进制的方法。设 \(k\) 满足 \(2^{k-1}\leq c<2^k\),那么我们把重量 \(\in[2^{k-1},2^k)\) 的分为重物品,重量 \(<2^{k-1}\) 的分为轻物品。关键性质是选取一个重物品之后 \(k\) 会立即降低 \(1\)
现在的问题变成了快速找到第一个可以选取的重物品(或者判断找不到),设现在选取到的问题是 \(x\),某个重物品的位置是 \(y\),那么如果 \([x,y)\) 之间轻物品的重量和\(+\)这个重物品的重量 \(\leq c\),这就是可以选取的重物品。
具体来说,就是用线段树维护区间内的重物品,满足它到区间左端点的轻物品重量和\(+\)这个重物品的重量的最小值。那么我们要维护 \(sw[i][j],sv[i][j],ex[i][j]\) 分别表示 \(k=j\) 时,区间的轻物品重量和 \(/\) 价值和 \(/\) 最小重物品的权值。然后直接在线段树上搜即可,在买得起区间全部物品,或者是买不起区间重物品时退出。
时间复杂度 \(O(n\log n\log c)\),实现细节可以参考代码,十分好理解。
总结
这类多次询问,按一定顺序选物品的问题,考虑让某一量迅速的下降,可以主动套用二进制之类 \(O(\log)\) 的东西。
多个过程的线段树操作,代码实现中可以直接在线段树上搜索,恰当时机退出即可,代码会好写很多。
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 200005;
const int N = 800005;
#define int long long
const int inf = 1e18;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,w[M],v[M],a[M],p[M],id[M];
int sw[N][18],sv[N][18],ex[N][18];
void upd(int i,int x)
{
for(int j=0;j<18;j++)
{
sw[i][j]=sv[i][j]=0;ex[i][j]=inf;
if(w[x]<(1<<j))
sw[i][j]+=a[x]*w[x],sv[i][j]+=a[x]*v[x];
else if(w[x]<(1<<j+1) && a[x])
ex[i][j]=w[x];
}
}
void up(int i)
{
for(int j=0;j<18;j++)
{
sw[i][j]=sw[i<<1][j]+sw[i<<1|1][j];
sv[i][j]=sv[i<<1][j]+sv[i<<1|1][j];
ex[i][j]=min(ex[i<<1][j],ex[i<<1|1][j]+sw[i<<1][j]);
}
}
void add(int i,int l,int r,int id,int c)
{
if(l==r) {a[p[l]]+=c;upd(i,p[l]);return ;}
int mid=(l+r)>>1;
if(mid>=id) add(i<<1,l,mid,id,c);
else add(i<<1|1,mid+1,r,id,c);
up(i);
}
int ask(int i,int l,int r,int &c)
{
if(l==r)
{
int x=min(a[p[l]],c/w[p[l]]);
c-=x*w[p[l]];return v[p[l]]*x;
}
int mid=(l+r)>>1;
while(k && (1<<k-1)>c) k--;
if(sw[i][k]<=c) return c-=sw[i][k],sv[i][k];
if(sw[i][k-1]<=c && ex[i][k-1]>c)
return c-=sw[i][k-1],sv[i][k-1];
return ask(i<<1,l,mid,c)+ask(i<<1|1,mid+1,r,c);
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
a[i]=read(),w[i]=read(),v[i]=read(),p[i]=i;
sort(p+1,p+1+n,[&](int i,int j)
{return v[i]==v[j]?w[i]<w[j]:v[i]>v[j];});
for(int i=1;i<=n;i++)
id[p[i]]=i,add(1,1,n,i,0);
while(m--)
{
int op=read(),x=read();
if(op==3)
{
k=17;
printf("%lld\n",ask(1,1,n,x));
continue;
}
int y=read();
add(1,1,n,id[y],(op==1)?x:-x);
}
}