The 2018 ICPC Asia-East Continent Final
A.Exotic … Ancient City(思路 並查集)
題面在這兒(或者去牛客 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(后綴數組/后綴自動機 零和博弈)
\(Description\)
題面在這兒(或者去牛客 EC-Final J上看)。
簡要題意:給定一個長為\(n\)的字符串,記\(s_i\)為從\(i\)開始的后綴。
兩個人進行博弈,先手任意確定一個概率序列\(p_i\geq0,\sum_{i=1}^np_i=1\)。后手確定一個后綴\(j\)。先手想要最大化下式的值,后手想要最小化下式的值,兩人按照最優策略決定,求最后下式的值:
多組數據,\(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;
}