zoukankan      html  css  js  c++  java
  • 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)
    当做到某一层没有要加入的边时,答案就不会改变了。
    所以我们就可以计算出每一层成功加入边数的增量的增量(即求一遍前缀和后我们可以得到每一层之间相差多少),这是个二阶差分,所以求两遍前缀和就可以得到每一层成功加入的边数了。
    枚举边权(0sim29)(30)遍即可。
    因为连通性至多改变(O(n))次?所以复杂度是对的,为(O(30malpha(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
    ",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_igeq0,sum_{i=1}^np_i=1)。后手确定一个后缀(j)。先手想要最大化下式的值,后手想要最小化下式的值,两人按照最优策略决定,求最后下式的值:

    [max_{{p_i}}left(min_{j=1}^nleft(sum_{k=1}^np_kmathbb{lcp}(s_k,s_j) ight) ight) ]

    多组数据,(nleq2 imes10^5,sum nleq 5 imes10^5)

    (Solution)
    一个最大化一个最小化,可以看做先手的收益是(sum_{k=1}^np_kmathbb{lcp}(s_k,s_j)),后手的收益是(-sum_{k=1}^np_kmathbb{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是(minlimits_{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(nlog 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
    ",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;
    }
    

    
    
  • 相关阅读:
    TCP同步与异步
    C#委托与事件
    线程
    C# 多人聊天程序
    vs启动错误
    记住我的痛苦
    C#命名空间与类名的冲突
    C#调试类
    linux ifconfig命令参数及用法详解linux查看配置网卡命令
    B/S架构
  • 原文地址:https://www.cnblogs.com/SovietPower/p/10269997.html
Copyright © 2011-2022 走看看