AtCoder Grand Contest 023

Preface

《有生之年系列》

没看题解Rush掉了所有题(好吧F看了眼确认了下算法的正确性然后找了种好实现的写)

STO CXR ORZ


A - Zero-Sum Ranges

《有手就行》

把所有前缀和扔到map里维护下个数即可

#include<cstdio>
#include<map>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int n,x; long long sum,ans; map <long long,int> c;
int main()
{
	RI i; for (scanf("%d",&n),c[0]=i=1;i<=n;++i)
	scanf("%d",&x),sum+=x,ans+=c[sum],++c[sum];
	return printf("%lld",ans),0;
}

B - Find Symmetries

《论我如何和看错题目的陈指导交谈甚欢》

刚开始想复杂了,后来陈指导看对题目之后马上就秒了

容易发现若一对\(A,B\)是合法的,那么\(A-1,B-1\)也一定合法(因为对角线没动,其它位置位移后显然还是对称)

因此我们现在只需要统计\(B=0\)时的方案数即可,最后乘上\(B\)即为答案

直接\(O(n^3)\)大暴力即可

#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=305;
int n,ans; char a[N][N],b[N][N];
int main()
{
	RI i,j,k; for (scanf("%d",&n),i=0;i<n;++i) scanf("%s",a[i]);
	for (k=0;k<n;++k)
	{
		for (i=0;i<n;++i) for (j=0;j<n;++j) b[i][(j+k)%n]=a[i][j];
		bool flag=1; for (i=0;i<n&&flag;++i) for (j=0;j<n&&flag;++j)
		if (b[i][j]!=b[j][i]) flag=0; ans+=flag;
	}
	return printf("%d",ans*n),0;
}

C - Painting Machines

《论我和陈指导如何写错同一个智障错误》

首先这种轮次的题目一眼容斥,我们考虑求出\(f_i\)表示\(\le i\)轮全部染色完的方案数

由于这题性质很好,我们可以反着容斥,直接从大到小容斥,\(f_i-=f_{i-1}\times (n-i)\)

那么现在考虑\(f_i\)怎么求,首先考虑\(1,n-1\)这两个机器是必选的,现在我们就要在中间加入\(i-2\)个机器还要满足排序后相邻两个机器差为\(1\)\(2\)

我们考虑把它转化为坐标系上走路的问题,这里起点就是\((1,1)\),终点是\((i,n-1)\),每次向右走\(1\)格然后再向上走\(1\)\(2\)

再转化一下就变成从\((1,1)\)走到\((i,n-1-i)\),每次向右走\(1\)格然后再向上走\(0\)\(1\)

容易发现此时方案数就是选择出某些时刻向上走,即\(C_{i-1}^{n-1-i}\),然后我们这里求出的有序的因此要乘上\(i!\)

因此最后答案就是\(\sum_{i=2}^{n-1} f_i\times i\times (n-1-i)!\),注意后面的\((n-1-i)!\)不能漏乘

#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,mod=1e9+7;
int n,fact[N],inv[N],f[N],ans;
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline void init(CI n)
{
	RI i; for (fact[0]=i=1;i<=n;++i) fact[i]=1LL*fact[i-1]*i%mod;
	for (inv[n]=quick_pow(fact[n]),i=n-1;~i;--i) inv[i]=1LL*inv[i+1]*(i+1)%mod;
}
inline int C(CI n,CI m)
{
	return 1LL*fact[n]*inv[m]%mod*inv[n-m]%mod;
}
int main()
{
	RI i; scanf("%d",&n); init(n); if (n==2) return puts("1"),0;
	for (i=2;i<n;++i) if (i-1>=n-1-i) f[i]=1LL*C(i-1,n-1-i)*fact[i]%mod;
	for (i=n-1;i>=2;--i) (f[i]-=1LL*f[i-1]*(n-i)%mod)%=mod,
	(f[i]+=mod)%=mod,(ans+=1LL*f[i]*i%mod*fact[n-1-i]%mod)%=mod;
	return printf("%d",ans),0;
}

D - Go Home

《关于我和陈指导在想题的同时竟完成绝世名画》

这道题首先我们要意识到一个点,每个人尽管使用最优策略但最后仍然是利己的,但同时他们的利己策略也并非无脑地直接往自家方向投票

乍一看我们毫无头绪,但简单分析一下我们发现:

  • \(S\)在剩下所有人的同一侧时,所有人都会选择让车往该去的方向并随着车开到最远的位置结束这个过程
  • \(S\)在中间,则最后被到达的人一定是当前所有人的两个端点之一

我们假设我们现在要处理\([l,r]\)的人,我们考虑\(p_l\)\(p_r\)的关系:

  • \(p_l<p_r\):此时若车在中间,而\(p_l\)右侧还有个\(p_{l+1}\),且\(p_l+p_{l+1}\ge p_r\)。此时若\(p_l\)的人选择让车往自己这个方向开把\(p_{l+1}\)的人全送走,车接下来就会开回\(x_r\)再开回来。而若\(p_l\)的人选择让车往\(p_r\)这个方向开把\(p_{r}\)的人全送走,车接下来就会开回\(p_l\)方向会让\(p_l\)的人得到更短的回家时间。稍加分析我们发现此时\(p_l\)的人到家的时间一定是\(p_r\)的人到家的时间加上一个常数,因此我们\(p_l\)的人显然会一直跟着\(p_r\)投票以便让他们更早到家,我们把\(p_r+=p_l\)之后再维护一下时间即可
  • \(p_l\ge p_r\):分析方法与上面完全相同,把\(p_l+=p_r\)再维护时间即可

递归处理问题,边界显然就是\(S\)在剩下所有人的同一侧,总复杂度\(O(n)\)

#include<cstdio>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
const long long INF=1e18;
int n,s,x[N]; long long t[N],p[N];
inline long long solve(CI l,CI r)
{
	if (s<x[l])
	{
		long long ans=0; for (RI i=l;i<=r;++i) ans=max(ans,x[i]-s+t[i]);
		return ans;
	}
	if (s>x[r])
	{
		long long ans=0; for (RI i=l;i<=r;++i) ans=max(ans,s-x[i]+t[i]);
		return ans;
	}
	if (p[l]<p[r]) return t[r]=max(t[r],t[l]+x[r]-x[l]),p[r]+=p[l],solve(l+1,r);
	else return t[l]=max(t[l],t[r]+x[r]-x[l]),p[l]+=p[r],solve(l,r-1);
}
int main()
{
	RI i; for (scanf("%d%d",&n,&s),i=1;i<=n;++i) scanf("%d%lld",&x[i],&p[i]);
	return printf("%lld",solve(1,n)),0;
}

E - Inversions

《论我如何为现实所迫改口对此题的评价》(有手就行->我人没了)

首先先来想一个\(O(n^2)\)暴力,我们先考虑一个子问题:满足限制的排列个数是多少?

我们考虑求一个\(c_i=\sum_{j=1}^n [a_j\ge i]\),那么最后合法的排列个数就是\(ret=\prod_{i=1}^ n (c_i-(n-i))\)

(考虑以上式子的组合意义:对于每个位置\(i\),可以放\(\ge i\)的数的位置有\(c_i\)个,有\(n-i\)个位置被比它大的数占掉了)

然后我们考虑逆序对怎么算,先考虑一个\(O(n^2)\)暴力(我也只能想想暴力了):

枚举一对位置\(i,j(i<j)\),我们考虑使得\(p_i>p_j\)的排列数就是这对位置的贡献,我们分类讨论:

  • \(a_i=a_j\):首先想到这种特殊情况就对下面的情况帮助很大了。因为此时\(i,j\)能填的数的范围是完全一致的,而所有填的数又不相同,因此此时\(p_i>p_j\)的方案数就是\(\frac{ret}{2}\)
  • \(a_i<a_j\),此时我们发现当\(p_j\)填到\(>a_i\)的数时\(p_i\)是不可能\(>p_j\)的。因此我们可以把\(a_j\)变成\(a_i\)然后求出此时的合法排列个数\(ret'\),此时\(p_i>p_j\)的方案数就是\(\frac{ret'}{2}\)
  • \(a_i>a_j\),同上,注意这里直接求不方便我们求反面,把\(a_i\to a_j\)之后用\(ret\)减去\(p_i<p_j\)的方案\(\frac{ret'}{2}\)就是答案

考虑\(ret'\)的计算较\(ret\)仅改变了一个值,而\(a_j\to a_i\)(或\(a_i\to a_j\))的变换在\(c_i\)数组上的体现就是\(c_k-=1,k\in[a_i+1,a_j]\)(或\(c_k-=1,k\in[a_j+1,a_i]\)

因此我们可以暴力\(O(n^2)\)预处理出\(fc_{l,r}=\prod_{i=l}^r (c_i-(n-i)),dc_{l,r}=\prod_{i=l}^r (c_i-(n-i)-1)\),每次就可以\(O(1)\)计算\(ret'\)

先给一份\(O(n^2)\)的代码

#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=2005,mod=1e9+7;
int n,a[N],c[N],fc[N][N],dc[N][N],ret,ans,inv2;
//fc[i]=c[1]*c[2]*...*c[i]; dc[i]=(c[1]-1)*(c[2]-1)*...*(c[i]-1)
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
int main()
{
	RI i,j; 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 (ret=i=1;i<=n;++i) 
	if (c[i]<=n-i) return puts("0"),0; else ret=1LL*ret*(c[i]-(n-i))%mod;
	for (i=1;i<=n;++i) for (fc[i][i]=(c[i]-(n-i)),j=i+1;j<=n;++j)
	fc[i][j]=1LL*fc[i][j-1]*(c[j]-(n-j))%mod;
	for (i=1;i<=n;++i) for (dc[i][i]=(c[i]-(n-i)-1),j=i+1;j<=n;++j)
	dc[i][j]=1LL*dc[i][j-1]*(c[j]-(n-j)-1)%mod;
	for (inv2=quick_pow(2),i=1;i<n;++i) for (j=i+1;j<=n;++j)
	if (a[i]==a[j]) (ans+=1LL*ret*inv2%mod)%=mod;
	else if (a[i]<a[j])
	{
		int cur=1LL*fc[1][a[i]]*dc[a[i]+1][a[j]]%mod*(a[j]<n?fc[a[j]+1][n]:1)%mod;
		(ans+=1LL*cur*inv2%mod)%=mod;
	} else
	{
		int cur=1LL*fc[1][a[j]]*dc[a[j]+1][a[i]]%mod*(a[i]<n?fc[a[i]+1][n]:1)%mod;
		(ans+=1LL*(ret-1LL*cur*inv2%mod+mod)%mod)%=mod;
	}
	return printf("%d",ans),0;
}

然后陈指导看了一眼就知道怎么优化了,而我还一脸懵逼

首先我们发现那个\(O(n^2)\)预处理的数组肯定不能开,但是我们之所以不开成前缀积的原因就是\(c_i-(n-i)-1\)有可能等于\(0\),某个位置等于\(0\)之后后面的结果就全乱掉了

但是我们先不管这个,再仔细一看算贡献的式子发现这个式子显然可以把\(a_i\)排序之后按序处理(因为此时\(a_i,a_j\)的贡献刚好可以拆成独立的两部分用树状数组维护)

于是我们想到把\(c_i-(n-i)-1=0\)的位置和\(a_i\)一起排序,把他看做一个断点来使得再某个位置之后的前缀积数组全部都要清空,并且统计之前答案的树状数组也要清空

因此这题就做完了,总复杂度\(O(n\log n)\),统计答案的时候还是蛮多细节的

#include<cstdio>
#include<algorithm>
#define RI int
#define CI const int&
using namespace std;
const int N=200005,mod=1e9+7;
struct element
{
	int v,id;
	inline element(CI V=0,CI Id=0) { v=V; id=Id; }
	friend inline bool operator < (const element& A,const element& B)
	{
		return A.v!=B.v?A.v<B.v:A.id<B.id;
	}
}a[N<<1]; int n,m,c[N],fc[N],ifc[N],dc[N],idc[N],ret,ans,inv2;
//fc[i]=c[1]*c[2]*...*c[i]; dc[i]=(c[1]-1)*(c[2]-1)*...*(c[i]-1)
class Tree_Array1
{
	private:
		int bit[N],stk[N],v[N],top;
	public:
		#define lowbit(x) (x&(-x))
		inline int get(RI x,int ret=0)
		{
			for (;x;x-=lowbit(x)) (ret+=bit[x])%=mod; return ret;
		}
		inline void add(RI x,CI y)
		{
			for (stk[++top]=x,v[top]=y;x<=n;x+=lowbit(x)) (bit[x]+=y)%=mod;
		}
		inline void _add(RI x,CI y)
		{
			for (;x<=n;x+=lowbit(x)) (bit[x]+=y)%=mod;
		}
		inline void clear(void)
		{
			while (top) _add(stk[top],mod-v[top]),--top;
		}
		#undef lowbit
}BIT1;
class Tree_Array2
{
	private:
		int bit[N],stk[N],v[N],top;
	public:
		#define lowbit(x) (x&(-x))
		inline int get(RI x,int ret=0)
		{
			for (;x<=n;x+=lowbit(x)) (ret+=bit[x])%=mod; return ret;
		}
		inline void add(RI x,CI y)
		{
			for (stk[++top]=x,v[top]=y;x;x-=lowbit(x)) (bit[x]+=y)%=mod;
		}
		inline void _add(RI x,CI y)
		{
			for (;x;x-=lowbit(x)) (bit[x]+=y)%=mod;
		}
		inline void clear(void)
		{
			while (top) _add(stk[top],mod-v[top]),--top;
		}
		#undef lowbit
}BIT2,BIT3;
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline int C2(CI x)
{
	return 1LL*x*(x-1)%mod*inv2%mod;
}
int main()
{
	RI i,j,k; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i].v),a[i].id=i,++c[a[i].v];
	for (i=n;i;--i) c[i]+=c[i+1]; for (ret=i=1;i<=n;++i) 
	if (c[i]<=n-i) return puts("0"),0; else ret=1LL*ret*(c[i]-(n-i))%mod;
	for (fc[0]=i=1;i<=n;++i) fc[i]=1LL*fc[i-1]*(c[i]-(n-i))%mod;
	for (i=0;i<=n;++i) ifc[i]=quick_pow(fc[i]);
	for (m=n,i=1;i<=n;++i) if (c[i]-(n-i)==1) a[++m]=element(i);
	else dc[i]=1LL*(dc[i-1]?dc[i-1]:1)*(c[i]-(n-i)-1)%mod,idc[i]=quick_pow(dc[i]);
	for (inv2=quick_pow(2),sort(a+1,a+m+1),i=1;i<=m;i=j+1)
	{
		if (!a[i].id) { BIT1.clear(); BIT2.clear(); j=i; continue; }
		for (j=i;j<m&&a[j].v==a[j+1].v;++j); (ans+=1LL*ret*inv2%mod*C2(j-i+1)%mod)%=mod;
		for (k=i;k<=j;++k) (ans+=1LL*fc[n]*ifc[a[k].v]%mod*dc[a[k].v]%mod*BIT1.get(a[k].id)%mod*inv2%mod)%=mod;
		for (k=i;k<=j;++k) (ans+=(1LL*BIT3.get(a[k].id)*ret%mod-1LL*fc[n]*ifc[a[k].v]%mod*dc[a[k].v]%mod*BIT2.get(a[k].id)%mod*inv2%mod+mod)%mod)%=mod;
		for (k=i;k<=j;++k) BIT1.add(a[k].id,1LL*fc[a[k].v]*idc[a[k].v]%mod),BIT2.add(a[k].id,1LL*fc[a[k].v]*idc[a[k].v]%mod),BIT3.add(a[k].id,1);
	}
	return printf("%d",ans),0;
}

F - 01 on Tree

《论我如何全程被陈指导带飞做出F》

刚开始又是题目看错又是分析错搞成字典序最小了,最后还是在陈指导的引领下找到了正确的路

从根开始找的过程显然可以反过来,变成从叶节点向上寻找,每次插在前面

我们考虑对于某个点,子树内的答案已经算好了,现在我们只要考虑合并不同子树内答案的时合并顺序带来的逆序对数差异

假设两个子树\(u,v\)\(0/1\)个数信息为\(u_{0/1},v_{0/1}\),那么当\(u\)\(v\)前逆序对数\(u_1\times v_0\)\(v\)\(u\)后逆序对数\(u_0\times v_1\),那么我们可以以此来确定合并的顺序

因此一个朴素的想法就是用可并堆维护子树内大小信息,然后每次向上合并的时候更新下答案,这样正确性是没问题的但是不够优美(主要是懒得写左偏树了)

我们考虑对全局开一个堆维护答案,每次合并用并查集来维护子树(联通块)内\(0/1\)个数

因为这题不强调所有子树同时合并到一个点,因此可以这样写,而且我们还可以利用类似DJ的标记方法来完成删除操作来避免写可删除堆

总之还是一道思维难度一般,实现巧妙的话代码难度也很小的题吧,感觉E比这个难多了

总复杂度\(O(n\log n)\)

#include<cstdio>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
struct element
{
	int c0,c1,id;
	inline element(CI C0=0,CI C1=0,CI Id=0) { c0=C0; c1=C1; id=Id; }
	friend inline bool operator < (const element& A,const element& B)
	{
		return 1LL*A.c1*B.c0>1LL*A.c0*B.c1;
	}
}; priority_queue <element> hp; int n,anc[N],x,fa[N],c[N][2]; long long ans;
inline int getfa(CI x)
{
	return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
int main()
{
	RI i; for (scanf("%d",&n),i=2;i<=n;++i) scanf("%d",&anc[i]);
	for (i=1;i<=n;++i) scanf("%d",&x),++c[i][x],fa[i]=i;
	for (i=2;i<=n;++i) hp.push(element(c[i][0],c[i][1],i));
	while (!hp.empty())
	{
		int x=getfa(hp.top().id),c0=hp.top().c0,c1=hp.top().c1; hp.pop();
		if (c[x][0]!=c0||c[x][1]!=c1) continue;
		int y=getfa(anc[x]); ans+=1LL*c[y][1]*c0;
		for (i=0;i<2;++i) c[y][i]+=c[x][i]; fa[x]=y;
		if (y!=1) hp.push(element(c[y][0],c[y][1],y));
	}
	return printf("%lld",ans),0;
}

Postscript

《所以我今天为什么那么喜欢书名号》

posted @ 2020-10-29 20:04  空気力学の詩  阅读(107)  评论(0编辑  收藏  举报