NOI On Line 提高组题解
(话说其实我想填的是去年CSP的坑...但是貌似有一道题我还不会写咕咕咕...
先写一下这一次的题解吧. T1:序列.题意省略。
两种操作。这种题要先分析部分分 给出了全部都是2操作的子任务。
发现A 2 B,B 2 C这个时候可以推到 A 2 C也就是所以被2相连的点都存在这种关系。
考虑缩点 把这些点都缩到一起表示他们的权值可以随便传递.
这个时候对于当前子任务我们可以很容易回答,就是看某个集合的权值和是否为0.
考虑有操作1的时候 A 1 B B 1 C 可以发现这可以转换成 A 2 C 转换成这样做之后我们再次把2操作进行缩点。
那么整张图变成了什么?对于1操作直接连边然后看能否缩点 2操作直接缩点 所以整张图变成了若干个联通块 且每个联通块至多只有两个点。
我们简单的分析一下什么时候有解,分类讨论一下即可。
值得一提的是对于1操作变2操作时我们连边需要拿2操作过后缩过的点连边,否则可能会出现(a,b) (c,d) 而c和d之前被缩起来了。
可以证明拿缩过点连边是正确的。(这个地方是坑点 考试的时候这个地方我就没考虑到。
```
const int MAXN=100010;
int n,m,cnt,flag,len,T;
int f[MAXN],vis[MAXN];ll a[MAXN];
int lin[MAXN],ver[MAXN<<1],nex[MAXN<<1];
struct wy{int x,y;}t[MAXN];
inline int getfather(int x){return x==f[x]?x:f[x]=getfather(f[x]);}
inline void merge(int x,int y)
{
int xx=getfather(x);
int yy=getfather(y);
if(xx==yy)return;
a[xx]+=a[yy];
f[yy]=xx;
}
inline void add(int x,int y)
{
ver[++len]=y;
nex[len]=lin[x];
lin[x]=len;
}
int main()
{
//freopen("1.in","r",stdin);
//freopen("sequence.out","w",stdout);
T=read();
while(T--)
{
n=read();m=read();len=flag=cnt=0;
for(int i=1;i<=n;++i)a[i]=read();
for(int i=1;i<=n;++i)
{
f[i]=i;vis[i]=0;
a[i]=a[i]-read();
lin[i]=0;
}
for(int i=1;i<=m;++i)
{
int op,x,y;
op=read();x=read();y=read();
if(op==2)merge(x,y);
else t[++cnt]=(wy){x,y};
}
//if(flag){puts("NO");continue;}
for(int i=1;i<=cnt;++i)
{
int x=getfather(t[i].x);
int y=getfather(t[i].y);
add(x,y);add(y,x);
}
for(int i=1;i<=n;++i)
{
int last=0;
for(int j=lin[i];j;j=nex[j])
{
int tn=ver[j];
if(last)merge(tn,last);
last=tn;
}
}
for(int i=1;i<=cnt;++i)
{
int x=getfather(t[i].x);
int y=getfather(t[i].y);
if(x==y)vis[x]=1;
}
for(int i=1;i<=n;++i)
{
int xx=getfather(i);
if(xx==i)
{
int ww=0;
for(int j=lin[i];j;j=nex[j])
{
int tn=getfather(ver[j]);
if(tn!=i)
{
ww=tn;
//if(ww!=tn)cout<<"ww"<
但是我们想要不去模拟就必须要知道每轮冒泡排序改变的是什么东西.
每一轮冒泡 整个序列中 未到位的最大值一定会交换到后面,这个时候考虑一下这个最大值所处的位置 如果为p的话,其应该到的位置为pos 那么说明p+1~pos这些数字对逆序对的贡献减一 (如果这个数字每次都在排头那么我们就可以不用知道序列是什么样子而直接求出来逆序对的变换情况了。
考虑这个最大值p之前的位置会怎么变换 可以发现还是一个较大值被交换到p+1这个位置然后轮到p开始交换。
至此我们不难发现每次冒泡某个数字之前如果有比其大的数字那么这种数字至少减少1.
设b[i]为i之前有多少个比i大的数字.那么进行k轮 每次每个位置上的b[i]都要减减.
答案=ans-$\sum{min(b[i],k)}$ans为初始序列的逆序对个数.这个显然可以利用线段树维护。
考虑修改 ans修改是O(1)的 b[i]的修改也仅限于一个位置 所以这是一个区间求和单点修改的问题.
树状数组好像需要两个才能维护 直接线段树即可. ``` const int MAXN=200010; int n,m; int a[MAXN],b[MAXN]; ll c[MAXN],ans; inline int ask(int x) { int cnt=0; while(x) { cnt+=c[x]; x-=x&(-x); } return cnt; } inline void add(int x,int y) { if(!x)return; while(x<=n) { c[x]+=y; x+=x&(-x); } } struct wy { int l,r; ll sum; ll add; }t[MAXN<<2]; void build(long long p,long long l,long long r) { t[p].l=l;t[p].r=r; if(r==l)return; ll mid=(l+r)>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); t[p].sum=t[p<<1].sum+t[p<<1|1].sum; } void spread(long long p) { if(t[p].add!=0) { t[p<<1].add+=t[p].add; t[p<<1|1].add+=t[p].add; t[p<<1].sum+=t[p].add*(t[p<<1].r-t[p<<1].l+1); t[p<<1|1].sum+=t[p].add*(t[p<<1|1].r-t[p<<1|1].l+1); t[p].add=0; } } void change(long long p,long long l,long long r,long long k) { if(l>r)return; if(l<=t[p].l&&r>=t[p].r) { t[p].add+=k; t[p].sum+=k*(t[p].r-t[p].l+1); return; } spread(p); long long mid=(t[p].l+t[p].r)>>1; if(l<=mid)change(p<<1,l,r,k); if(r>mid)change(p<<1|1,l,r,k); t[p].sum=t[p<<1].sum+t[p<<1|1].sum; } long long ask(long long p,long long l,long long r) { if(l>r)return 0; if(l<=t[p].l&&r>=t[p].r)return t[p].sum; spread(p); long long mid=(t[p].l+t[p].r)>>1; long long ans=0; if(l<=mid)ans+=ask(p<<1,l,r); if(r>mid)ans+=ask(p<<1|1,l,r); return ans; } int main() { //freopen("bubble.in","r",stdin); //freopen("bubble.out","w",stdout); n=read();m=read(); for(int i=1;i<=n;++i) { a[i]=read(); b[i]=ask(n-a[i]+1); add(n-a[i]+1,1); ans+=b[i]; } build(1,1,n); for(int i=1;i<=n;++i)change(1,1,b[i],1); for(int i=1;i<=m;++i) { int op,x; op=read();x=read(); if(op==1) { if(a[x]>a[x+1]) { change(1,b[x+1],b[x+1],-1); --b[x+1];--ans; swap(a[x],a[x+1]); swap(b[x],b[x+1]); } else { change(1,b[x]+1,b[x]+1,1); ++b[x];++ans; swap(a[x],a[x+1]); swap(b[x],b[x+1]); } } else { x=min(x,n); ll w=ask(1,1,x); printf("%lld\n",ans-w); } } return 0; } ``` ps:发现这个代码中的线段树长的不太好看了吧,这是很早以前的代码了,考试的时候为了节省时间直接粘了个板子. T3:最小环.
当时写的时候时间不太够用了 因为T1debug时间过长。。
观察子任务有两个点k==1 简单分析一下什么时候形成的环最大 amax-4,amax-2,amax,amax-1,amax-3...这样排列。
这样计算可以发现能把样例算出来,关于证明:交换任意两个数的位置得到的结果不会比原来更优 所以这样排列就是最优的。
还有一个子任务n为偶数且k==2 我们发现此时形成了两个环。把n分一半套用上述k==1的做法即可解决。
对于$k\leq \frac{n}{2}$我们发现n和k互质的时候也只会形成一个环和k==1一样。
我们只需要知道对于一个k来说最终形成的环的个数有多少个 再把数分开单独计算每一个环即可。
可以发现形成环的个数为 gcd(n,k).
$n\leq 200000$ d(n)最多280..每次暴力计算每个环O(n) 所以总复杂度$d(n)\cdot n$.d(n)表示n的因子个数。 ``` const ll MAXN=200010; ll n,m; ll a[MAXN],sum; ll ans[MAXN]; inline ll cmp(ll a,ll b){return a>b;} inline ll gcd(ll a,ll b){return b?gcd(b,a%b):a;} int main() { //freopen("1.in","r",stdin); //freopen("ring.out","w",stdout); n=read();m=read(); for(ll i=1;i<=n;++i)a[i]=read(),sum+=a[i]*a[i]; sort(a+1,a+1+n,cmp); for(ll i=1;i<=n/2;++i) { int ww=gcd(n,i); if(ww!=1||i==1) { if(ans[ww])continue; ll v=n/ww; ll cnt=0; for(ll w=1;w<=ww;++w)//一共有i组每组v个 { ll st=(w-1)*v+1; ll en=w*v; for(ll j=st;j<=en-2;++j)cnt+=a[j]*a[j+2]; cnt+=a[st]*a[st+1]; cnt+=a[en]*a[en-1]; } ans[ww]=cnt; } } for(ll i=1;i<=m;++i) { ll x=read(); if(!x){printf("%lld\n",sum);continue;} int ww=gcd(x,n); printf("%lld\n",ans[ww]); } return 0; } ```