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;
}



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM