【AtCoder】AtCoder Grand Contest 023 解题报告
\(A\):Zero-Sum Ranges(点此看题面)
- 给定一个长度为\(n\)的序列\(a\)。
- 求有多少对\(i,j(i<j)\)满足\(\sum_{x=i}^ja_x=0\)。
- \(n\le10^5\)
签到题
众所周知,记\(s_i=\sum_{x=1}^ia_x\),则\(\sum_{x=i}^j=s_j-s_{i-1}\)。
也就是求有多少对\(s\)相等。
直接开个\(map\)就好了。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define LL long long
using namespace std;
int n;map<LL,int> p;
int main()
{
RI i,x;LL s=0,ans=0;for(scanf("%d",&n),p[0]=i=1;i<=n;++i)//初始化p[0]=1,因为s[0]=0
scanf("%d",&x),ans+=p[s+=x]++;return printf("%lld\n",ans),0;//计算前缀和,并统计答案
}
\(B\):Find Symmetries(点此看题面)
- 给定一个\(n\times n\)的矩阵。
- 定义一个操作\((A,B)\),表示将矩阵最下面\(A\)行移到上面,最右面\(B\)列移到左面。
- 问有多少对\((A,B)\)得到的矩阵满足\(\forall i,j\in[1,n],a_{i,j}=a_{j,i}\)(\(A,B\in[0,n-1]\))。
- \(n\le300\)
签到题
智障如我,一开始居然看错了题目。。。即便看错题目居然还能和闪总谈笑风生讨论了好久。。。
显然如果\((A,B)\)合法,则\((A-1,B-1)\)必然也合法。
因此我们只要求出有多少\((A,0)\)合法,然后把答案乘上\(n\)即可。
代码:\(O(n^3)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 300
using namespace std;
int n;char s[N+5][N+5];
int main()
{
RI i,j,k,t=0,fg;for(scanf("%d",&n),i=0;i^n;++i) scanf("%s",s[i]);//读入原矩阵
for(k=0;k^n;t+=fg,++k) for(fg=1,i=0;i^n&&fg;++i)//枚举操作(k,0)
for(j=0;j^n&&fg;++j) s[(i+k)%n][j]^s[(j+k)%n][i]&&(fg=0);//判断是否合法
return printf("%d\n",t*n),0;//给答案乘上n
}
\(C\):Painting Machines(点此看题面)
- 有一张有\(n\)个格子的纸条。
- 有\(n-1\)个机器人,第\(i\)个机器人会染黑第\(i\)个和第\(i+1\)个格子。
- 对于一个\(n-1\)的排列\(P\),定义其价值为该排列中第几个机器人结束操作后整张纸条将首次被完全染黑。
- 求所有排列价值总和。
- \(n\le10^6\)
容斥
首先考虑求恰好第\(i\)个机器人结束不太容易,所以我们求在前\(i\)个机器人结束的方案数,然后容斥。
假设在前\(i\)个机器人结束的方案数为\(Calc(i)\)。
那么\(Calc(i)\)肯定要排除\(Calc(i-1)\)的贡献,在前\(i-1\)个机器人放了之后还剩下第\(n-i\)个机器人,因此第\(i\)个机器人有\(n-i\)种选法。
也就是说:
注意后面的\((n-1-i)!\),因为\(Calc(i)\)只考虑放了前\(i\)个机器人的方案数,而之后的\(n-1-i\)个机器人可以随意放。(一开始忘记写这个东西调了半天,还以为自己容斥写错了。。。)
\(Calc(i)\)
前面光讲了容斥,接下来考虑如何求\(Calc(i)\)。
假设我们把这\(i\)个机器人排序,那么就要满足下面两个要求:
- \(P_1=1,P_{i}=n-1\)。(因为没有这两个机器人不可能染黑第\(1\)个格子和第\(n\)个格子)
- \(\forall i\in[2,i],P_i=P_{i-1}+1\)或\(P_{i-1}+2\)。(假如\(P_i>P_{i-1}+2\),则第\(P_{i-1}+2\)个格子将无法被染黑)
以\((x,P_x)\)为点坐标,把这个看成一个平面直角坐标系,发现就变成每次向右走一步,同时向上走一或两步。
那么干脆把每次向上走都减少一步,即以\((x,P_x-x)\)为点坐标,每次向右走一步,可以向上走一步。
发现总共向右走\(i-1\)步,要向上走\((n-1-i)-(1-1)=n-1-i\)步,因此方案数为\(C_{i-1}^{n-1-i}\)。
但注意我们这里将机器人排序了,实际上还需要将这个方案数乘上\(i!\)表示打乱顺序。
最终得到:
代码:\(O(n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1000000
#define X 1000000007
#define C(x,y) (1LL*Fac[x]*IFac[y]%X*IFac[(x)-(y)]%X)//组合数
using namespace std;
int n,Fac[N+5],IFac[N+5];
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
int main()
{
RI i,t=0;if(scanf("%d",&n),n==2) return puts("1"),0;//特判n=2
for(Fac[0]=i=1;i<=n;++i) Fac[i]=1LL*Fac[i-1]*i%X;//预处理阶乘
for(IFac[n]=QP(Fac[n],X-2),i=n-1;~i;--i) IFac[i]=1LL*IFac[i+1]*(i+1)%X;//预处理阶乘逆元
#define Calc(x) ((x)>=n-(x)?1LL*Fac[x]*C(x-1,n-(x)-1)%X:0)//计算在前i个机器人结束的方案数
for(i=(n+1)/2;i^n;++i) t=(1LL*i*(Calc(i)-1LL*(n-i)*Calc(i-1)%X+X)%X*Fac[n-1-i]+t)%X;//容斥,统计答案
return printf("%d\n",t),0;
}
\(D\):Go Home(点此看题面)
- 有\(n\)个房子,第\(i\)个房子坐标为\(a_i\),其中有\(b_i\)个人。
- 有一辆公交车,初始坐标为\(s\),所有人都在车中。
- 每一时刻每个人会参与投票,决定车沿哪一方向行进(如果负方向的人大于等于正方向的人则往负方向,否则往正方向)。
- 每个人都会采取最优策略让自己能最早到家,问最迟到家的人到家时间。
- \(n\le10^5\)
\(\%\%\%hl666\%\%\%\)
基本结论(\(by\ hl666\))
考虑如果某一时刻所有房子都在车的同一边,车必然是直接一路到底。
否则,假设此时最左边的房子人数为\(b_l\),最右边的房子人数为\(b_r\),如果\(b_l\ge b_r\),我们发现必然会先访问\(l\),再一路向右访问\(r\)。(同理,如果\(b_l<b_r\),必然会先访问\(r\),再一路向左访问\(l\),这里以前一种情况为例)
那么\(r\)想让自己尽早回家,就是要让\(l\)尽早回家,所以必然会跟着\(l\)投票。
因此我们只要不断递归下去,每次都可以将某一边的一些人变成另一端点的人的附庸,这样就做完了。
代码:\(O(n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define LL long long
using namespace std;
int n,s,a[N+5];LL b[N+5];
I LL Solve(RI l,RI r)//递归
{
if(s<=a[l]) return a[r]-s;if(s>=a[r]) return s-a[l];//如果所有房子在车的同一边,直接一路到底
RI t=a[r]-a[l];if(b[l]>=b[r]) W(a[r]>s&&b[l]>=b[r]) b[l]+=b[r--];//把右边一些人变成左端点的附庸
else W(a[l]<s&&b[l]<b[r]) b[r]+=b[l++];return Solve(l,r)+t;//把左边一些人变成右端点的附庸,继续递归
}
int main()
{
RI i;for(scanf("%d%d",&n,&s),i=1;i<=n;++i) scanf("%d%d",a+i,b+i);
return printf("%lld\n",Solve(1,n)),0;
}
\(E\):Inversions(点此看题面)
- 给定一个长度为\(n\)的序列\(a\)。
- 询问对于所有满足\(\forall i\in[1,n],p_i\le a_i\)的排列\(p\),它们的逆序对总和是多少。
- \(n\le2\times10^5\)
\(\%\%\%hl666\%\%\%\)
开题思路
对于这种题目,一开始有两大思路:
- 枚举\(i,j\)两个位置求出它们产生贡献的方案数。
- 从小到大加数,则每次加入的数都是最大的。
绞尽脑汁思考半天,不知该抓住哪种思路深度思考,最后发现正解似乎差不多是这两种思路的结合?
暴力做法(\(by\ hl666\))
考虑一个子问题:求满足限制的排列个数。
令\(c_i=\sum_{x=1}^n[a_x\ge i]\),则合法排列个数就是\(\prod_{i=1}^n(c_i-(n-i))\)。(即对于每个\(i\),可以放大于等于\(i\)的位置有\(c_i\)个,其中有\(n-i\)个位置已经被比它大的数占掉了)
一个\(O(n^2)\)的做法,就是枚举位置\(i,j\)分类讨论它们的贡献:
- \(a_i=a_j\):容易发现\(p_i>p_j\)与\(p_i<p_j\)的方案一一对应,因此方案数为\(\frac{res}2\)。
- \(a_i<a_j\):因为当\(p_j>a_i\)时肯定没有贡献,所以我们要求出\(p_j\le a_i\)时的新方案数\(res'\)就转化成上面的情况了,方案数为\(\frac{res'}2\)。
- \(a_i>a_j\):恰好与第二种情况相反,只要从反面考虑,就变成了\(res-\frac{res'}2\)。
对于\(res'\),我们其实只修改了一个\(a_j\),也就是将\(a_i+1\sim a_j\)的\(c\)减\(1\),这可以预处理。
于是我们就得到了一个\(O(n^2)\)暴力。
优化做法
我们发现\(\prod_{i=1}^n(c_i-(n-i))\)是可以前缀积预处理的,因为如果存在\(\le0\)的项可以直接特判输出\(0\)。
但是\(c_i-(n-i)-1\)可能等于\(0\),这时候直接前缀积就会原地爆炸。
所以我们考虑将\(c_i-(n-i)-1\)按\(0\)分成若干段,隔段之间贡献必然为\(0\),那么就只要考虑段内贡献,且必然不会出现\(0\)的问题。
因此我们按\(a_i\)从小到大枚举,然后发现之前暴力推出来的式子可以拆成只跟\(i\)有关与只跟\(j\)有关两类项,仅和\(i\)与\(j\)大小关系有关。
于是只要树状数组优化就可以了。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define X 1000000007
#define I2 500000004LL
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
using namespace std;
int n,a[N+5],o[N+5],c[N+5],f[N+5],g[N+5],d[N+5],b[N+5],q,p[N+5];
I bool cmp(CI x,CI y) {return a[x]<a[y];}
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
struct TreeArray1//前缀树状数组
{
int a[N+5];I void U(RI x,CI y) {W(x<=n) Inc(a[x],y),x+=x&-x;}
I int Q(RI x,RI t=0) {W(x) Inc(t,a[x]),x-=x&-x;return t;}
}T1;
struct TreeArray2//后缀树状数组
{
int a[N+5];I void U(RI x,CI y) {W(x) Inc(a[x],y),x-=x&-x;}
I int Q(RI x,RI t=0) {W(x<=n) Inc(t,a[x]),x+=x&-x;return t;}
}T2,T3;
int main()
RI i;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d",a+i),++c[a[i]];for(i=n;i;--i) c[i]+=c[i+1];
for(i=1;i<=n;++i) if(c[i]<=n-i) return puts("0"),0;//判无解
for(f[0]=i=1;i<=n;++i) f[i]=1LL*f[i-1]*(c[i]-(n-i))%X,g[i]=QP(f[i],X-2);//预处理c[i]-(n-i)的前缀积
for(i=1;i<=n;++i) c[i]-(n-i)-1?b[i]=QP(d[i]=1LL*(p[q]^(i-1)?d[i-1]:1)*(c[i]-(n-i)-1)%X,X-2):p[++q]=i;//预处理c[i]-(n-i)-1的前缀积,同时按0分段
RI j,x,t=1,lst=1,tot=1,ans=0,s1,s2;for(i=1;i<=n;++i) o[i]=i;for(sort(o+1,o+n+1,cmp),i=1;i<=n;i=j)//按a[i]从小到大枚举
{
if(x=a[o[i]],t<=q&&p[t]<=x)//隔段
{
for(j=lst,lst=i;j^i;++j) T1.U(o[j],(X-1LL*f[a[o[j]]]*b[a[o[j]]]%X)%X),//清空
T2.U(o[j],(X-1LL*f[a[o[j]]]*b[a[o[j]]]%X)%X);W(t<=q&&p[t]<=x) ++t;
}
for(j=i;j<=n&&a[o[j]]==x;++j) s1=I2*f[n]%X*g[x]%X*d[x]%X*T1.Q(o[j]-1)%X,//注意相同a[i]一起处理
s2=(1LL*f[n]*T3.Q(o[j]+1)-I2*f[n]%X*g[x]%X*d[x]%X*T2.Q(o[j]+1)%X+X)%X,ans=(1LL*ans+s1+s2)%X;//统计答案
for(ans=(I2*I2%X*(j-i)%X*(j-i-1)%X*f[n]+ans)%X,j=i;j<=n&&a[o[j]]==x;++j)//统计相同a[i]位置之间的答案
T1.U(o[j],1LL*f[x]*b[x]%X),T2.U(o[j],1LL*f[x]*b[x]%X),T3.U(o[j],1);//修改
}return printf("%d\n",ans),0;
}
\(F\):01 on Tree(点此看题面)
- 给定一棵\(n\)个点的有根树,每个点有一个权值\(0\)或\(1\)。
- 每次随意删去一个没有父节点的点,把它的权值加在序列末端。
- 求最终生成序列逆序对个数的最小值。
- \(n\le2\times10^5\)
从下往上
不愧是我,一开始又看错题目。。。
直接从上往下考虑并不好做,因此我们从下往上做。
记得做的时候我还推了挺久,来证明在子树合并时同一棵子树是可以看作一个整体转移的,现在有点懒就不想给出具体步骤了。
考虑对于两个不同的子树\(A,B\),谁放在前面更优。
记录\(t0,t1\)分别为子树内\(0,1\)的个数。
发现两种情况产生的逆序对个数分别为\(t1(A)\times t0(B)\)和\(t0(A)\times t1(B)\),如果\(A\)放在前面说明:
也就是说我们根据\(\frac{t0}{t1}\)开个堆,只要写个可并堆这道题就做完了。
一个不用可并堆的做法
写个可并堆毕竟还是有点麻烦。。。
在这道题中,转移其实并没有顺序,因此我们可以搞一个堆+并查集的奇怪玩意来取代可并堆。
具体就是每次选出\({\frac{t0}{t1}}\)最大的节点与其父节点合并。但由于其原树上的父节点可能已经合并掉了,因此还需要开一个并查集来找实际的父节点。
当然如果觉得这种做法太莫名其妙了直接写可并堆也是可以的。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define LL long long
using namespace std;
int n,a[N+5],fa[N+5],cnt[N+5][2];
int f[N+5];I int getfa(CI x) {return f[x]?f[x]=getfa(f[x]):x;}//并查集
struct Data
{
int p,t0,t1;I Data(CI x=0):p(x),t0(cnt[x][0]),t1(cnt[x][1]){}
I bool operator < (Con Data& o) Con {return 1LL*t0*o.t1<1LL*t1*o.t0;}
};priority_queue<Data> q;
int main()
{
RI i;for(scanf("%d",&n),i=2;i<=n;++i) scanf("%d",fa+i);
for(i=1;i<=n;++i) scanf("%d",a+i),++cnt[i][a[i]],i^1&&(q.push(Data(i)),0);//初始化堆
RI x,y;Data t;long long ans=0;W(!q.empty())//不断操作直至堆为空
{
if(t=q.top(),q.pop(),x=getfa(t.p),cnt[x][0]^t.t0||cnt[x][1]^t.t1) continue;//类似于Dijkstra,如果取出的是假元素
y=getfa(fa[x]),ans+=1LL*cnt[y][1]*cnt[x][0],cnt[y][0]+=cnt[x][0],cnt[y][1]+=cnt[x][1],//更新答案,更新父节点信息
f[x]=y,y>1&&(q.push(Data(y)),0);//将新元素放入堆中
}return printf("%lld\n",ans),0;
}