博客园 首页 私信博主 显示目录 隐藏目录 管理 动画

2018 EC-Final 部分题解 (A,J)


The 2018 ICPC Asia-East Continent Final

比赛链接

A.Exotic … Ancient City(思路 并查集)

https://www.bilibili.com/video/av38542305 A

题面在这儿(或者去牛客 EC-Final A上看)。

边权只有\(30\),这其实和边权为\(1\)是等价的。也就是去算边权为\(1\)的边用了多少条,边权为\(2\)的边用了多少条...设只考虑边权为\(1\)的边用了\(x\)条,只考虑边权为\(1,2\)的边一共用了\(y\)条,那么边权为\(2\)的边就用了\(y-x\)条。
其实可以这样算:枚举边权\(w\),将所有边权\(\leq w\)的边加入图中,若成功加入了\(t\)条边(要构造一棵树,可以直接并查集维护),设当前图的点数为\(n\),则边权\(>w\)的边一共用了\(n-1-t\)条。令它们贡献为\(1\),即\(Ans\)+=\(n-1-t\)
这样\(w\)\(0\)枚举到\(30\),那边权为\(1\)的边就会被算\(1\)次,边权为\(2\)的边会被算\(2\)次...每一次的贡献都是直接加\(1\),不需要考虑边权。
对于当前要加入的边,我们维护一个大小为\(2n\)的并查集,计算每一层成功加入了多少条边。
对于最初的两层点,只需要并查集维护一下就可以了。考虑第二层与第三层点的连通性与前两层有什么不同,显然在之前连通的点现在还是能连通(加入的边都是一样的),区别就是,第二层的点之间可能在之前连通了。
所以我们可以保留之前的并查集,对于在第二层右侧连通的点对\(u_i+n,v_i+n\),在第三层左侧加入一条边\(u_i,v_i\)。原本的边就不需要保留了。
同样在第三层继续加边\(u,v\)的时候会出现两种情况:\(u,v\)不连通,合并\(u,v\)\(u,v\)已连通,这意味着和上一层相比我们可以少加一条边,令增量\(s[i]\)--。
同样对于在右侧连通的点对\(u_i+n,v_i+n\),再在第四层左侧加入边\(u_i,v_i\)
当做到某一层没有要加入的边时,答案就不会改变了。
所以我们就可以计算出每一层成功加入边数的增量的增量(即求一遍前缀和后我们可以得到每一层之间相差多少),这是个二阶差分,所以求两遍前缀和就可以得到每一层成功加入的边数了。
枚举边权\(0\sim29\)\(30\)遍即可。
因为连通性至多改变\(O(n)\)次?所以复杂度是对的,为\(O(30m\alpha(n))\)

//4.25s	28.3MB(301ms	28176KB)
#include <cstdio>
#include <cctype>
#include <vector>
#include <cstring>
#include <algorithm>
//#define gc() getchar()
#define MAXIN 300000
#define gc() (SS==TT&&(TT=(SS=IN)+fread(IN,1,MAXIN,stdin),SS==TT)?EOF:*SS++)
#define mp std::make_pair
#define pr std::pair<int,int>
typedef long long LL;
const int N=2e5+5,M=1e5+5;

int F[N];
LL Ans[M],s[M];
std::vector<pr> e[30],A,B;
char IN[MAXIN],*SS=IN,*TT=IN;

inline int read()
{
	int now=0;register char c=gc();
	for(;!isdigit(c);c=gc());
	for(;isdigit(c);now=now*10+c-'0',c=gc());
	return now;
}
int Find(int x)
{
	return x==F[x]?x:F[x]=Find(F[x]);
}

int main()
{
//	freopen("c.in","r",stdin);
//	freopen("c.out","w",stdout);

	const int n=read(),m=read(),et=read();
	for(int i=1; i<=et; ++i)
	{
		int u=read(),v=read(),w=read();
		for(int j=w; j<30; ++j) e[j].push_back(mp(u,v+n));
	}
	for(int w=0; w<30; ++w)
	{
		memset(s,0,sizeof s);
		for(int i=1; i<=2*n; ++i) F[i]=i;
		A.clear();
		for(int i=0,l=e[w].size(),u,v; i<l; ++i)
		{
			u=Find(e[w][i].first), v=Find(e[w][i].second);
			if(u>v) std::swap(u,v);
			if(u!=v)
			{
				++s[1], F[u]=v;
				if(u>n && v>n) A.push_back(mp(u-n,v-n));
			}
		}
		for(int d=2,l; (l=A.size()); ++d)
		{
			B=A, A.clear();
			for(int i=0,u,v; i<l; ++i)
			{
				u=Find(B[i].first), v=Find(B[i].second);
				if(u>v) std::swap(u,v);
				if(u!=v)
				{
					F[u]=v;
					if(u>n && v>n) A.push_back(mp(u-n,v-n));
				}
				else --s[d];
			}
		}
		for(int i=1; i<=m; ++i) s[i]+=s[i-1];
		for(int i=1; i<=m; ++i) s[i]+=s[i-1];
		for(int i=1; i<=m; ++i) Ans[i]+=1ll*(i+1)*n-1-s[i];
	}
	for(int i=1; i<=m; ++i) printf("%lld\n",Ans[i]);

	return 0;
}

J.Philosophical … Balance(后缀数组/后缀自动机 零和博弈)

https://www.bilibili.com/video/av38542305 44:28 J

\(Description\)
题面在这儿(或者去牛客 EC-Final J上看)。
简要题意:给定一个长为\(n\)的字符串,记\(s_i\)为从\(i\)开始的后缀。
两个人进行博弈,先手任意确定一个概率序列\(p_i\geq0,\sum_{i=1}^np_i=1\)。后手确定一个后缀\(j\)。先手想要最大化下式的值,后手想要最小化下式的值,两人按照最优策略决定,求最后下式的值:

\[\max_{\{p_i\}}\left(\min_{j=1}^n\left(\sum_{k=1}^np_k\mathbb{lcp}(s_k,s_j)\right)\right) \]

多组数据,\(n\leq2\times10^5,\sum n\leq 5\times10^5\)

\(Solution\)
一个最大化一个最小化,可以看做先手的收益是\(\sum_{k=1}^np_k\mathbb{lcp}(s_k,s_j)\),后手的收益是\(-\sum_{k=1}^np_k\mathbb{lcp}(s_k,s_j)\),所以这就是一个零和博弈,答案会在纳什均衡点处取到。即第一个人选择一种混合策略,使得自己在最坏情况下收益最大,也就是对面不管怎么决策,收益都一样。
SAM:
大概就是找出后缀树上的后缀节点,然后令它们的收益相同,从而解出各个位置的\(p\)和收益。(然而我不知道用SAM怎么标记后缀节点QAQ哪位dalao教我一下怎么用SAM写啊QAQ)
复杂度\(O(n)\)

(随便记的忽略下面这一段吧)
\(p_1f_1=p_2f_2=p_3f_3,\quad p_1+p_2+p_3=1\)
\(pf_1=(1-p)f_2\)
求出这个的\(p\),然后令\(f'=pf_1\),求\(pf'=(1-p)f_3\),算出来的\(1-p\)就是原方程的\(p_3\)

SA:
我们知道排名在\([l,r]\)中的后缀的LCP是\(\min\limits_{i=l+1}^r\{height_i\}\),设最小值的位置是\(p\),如果当前的两个后缀\(j,k\)\(p\)的两边,那么此时的LCP就是\(height_p\);否则\(j,k\)在同侧,可以考虑递归到两边去做。
能够想到的是,局部最优决策下的概率比等于全局最优决策下的概率比。也就是说令左边子区间在最优决策下,各位置的概率比为\(p_1:p_2:p_3...\),那么在于右区间合并,也就是在全局中,左区间各位置的概率比仍为\(p_1:p_2:p_3...\)
这个结论不难证明,当\(j,k\)同在左区间中的时候,答案是和右区间无关的(否则\(j,k\)有一个在右区间的话,答案和\(height_p\)有关,等会再讨论)。
所以我们可以分治去做,最后合并左右两区间,也就是\(j,k\)\(p\)的异侧的时候。
设左区间的答案为\(L\),整体分到的概率是\(x\),右区间的答案是\(R\),整体分到的概率就是\(1-x\)
假如后手选择\(j\)在左区间,那么\(k\)在右区间时先手的收益是\(Lx+ht_p(1-x)\)\(j\)在右区间,收益就是\(R(1-x)+ht_px\)
先手会令这两个式子相等,所以我们能解出\(x\),然后代到左式或右式就能得到当前区间的答案了。
分治复杂度\(O(n)\),建\(SA\)的复杂度\(O(n\log n)\)

//164ms	27744KB
#include <cstdio>
#include <cstring>
#include <algorithm>
typedef long long LL;
const int N=2e5+5;

int Log[N];
struct Suffix_Array
{
	int n,tm[N],sa[N],sa2[N],rk[N],ht[N],pos[N][18];
	char s[N];

	void Build()
	{
		scanf("%s",s+1), n=strlen(s+1);
//		memset(rk,0,std::min(N,n*2)<<2);//!
//		memset(sa2,0,std::min(N,n*2)<<2);

		int m=26,*x=rk,*y=sa2;
		for(int i=0; i<=m; ++i) tm[i]=0;
		for(int i=1; i<=n; ++i) ++tm[x[i]=s[i]-'a'+1];
		for(int i=1; i<=m; ++i) tm[i]+=tm[i-1];
		for(int i=n; i; --i) sa[tm[x[i]]--]=i;
		for(int p=0,k=1; k<n; k<<=1,m=p,p=0)
		{
			for(int i=n-k+1; i<=n; ++i) y[++p]=i;
			for(int i=1; i<=n; ++i) if(sa[i]>k) y[++p]=sa[i]-k;

			for(int i=0; i<=m; ++i) tm[i]=0;
			for(int i=1; i<=n; ++i) ++tm[x[i]];
			for(int i=1; i<=m; ++i) tm[i]+=tm[i-1];
			for(int i=n; i; --i) sa[tm[x[y[i]]]--]=y[i];

			std::swap(x,y), x[sa[1]]=p=1;
			for(int i=2; i<=n; ++i)
				x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&sa[i]+k<=n&&sa[i-1]+k<=n&&y[sa[i]+k]==y[sa[i-1]+k])?p:++p;//如果不清空要这么写 
//				x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?p:++p;
			if(p>=n) break;
		}
		for(int i=1; i<=n; ++i) rk[sa[i]]=i;
		ht[1]=0;
		for(int k=0,i=1,p; i<=n; ++i)
		{
			if(rk[i]==1) continue;
			if(k) --k;
			p=sa[rk[i]-1];
			while(i+k<=n && p+k<=n && s[i+k]==s[p+k]) ++k;
			ht[rk[i]]=k;
		}
	}
	inline int Min(int x,int y)
	{
		return ht[x]<ht[y]?x:y;
	}
	inline int QueryMin(int l,int r)
	{
		int k=Log[r-l+1];
		return Min(pos[l][k],pos[r-(1<<k)+1][k]);
	}
	void Init_ST(const int n)
	{
		for(int i=1; i<=n; ++i) pos[i][0]=i;
		for(int j=1; j<=Log[n]; ++j)
			for(int t=1<<j-1,i=n-t; i; --i)
				pos[i][j]=Min(pos[i][j-1],pos[i+t][j-1]);
	}
	double Solve(int l,int r)
	{
		if(l==r) return n-sa[l]+1;
		int p=QueryMin(l+1,r);
		double L=Solve(l,p-1),R=Solve(p,r),v=ht[p];
		return (L*R-v*v)/(L+R-2*v);
	}
	void Work()
	{
		Build(), Init_ST(n), printf("%.11lf\n",Solve(1,n));
	}
}sa;

int main()
{
//	freopen("bb.in","r",stdin);
//	freopen("bb.out","w",stdout);

	for(int i=2; i<N; ++i) Log[i]=Log[i>>1]+1;
	int T; scanf("%d",&T);
	while(T--) sa.Work();

	return 0;
}


posted @ 2019-01-15 08:17  SovietPower  阅读(2099)  评论(0编辑  收藏  举报