白銀之春 Solution
比賽用題面、題解、標程和資料生成器都掛在 git@github.com:sun123zxy/spring.git 上。
Problem
白銀之春 (spring.cpp/.in/.out) (2s,512MB)
Background
妖夢正在收集春度!
Description
幻想鄉由 \(n\) 個地點和 \(m\) 條單向小路組成,第 \(i\) 個地點蘊含著 \(s_i\) 的春度。妖夢從位於 \(1\) 號節點的白玉樓出發,沿圖上路徑收集沿路的春度,總春度為收集到的所有春度之和。
半人半靈的妖夢具有一種名叫“人妖槽”的屬性,該屬性有兩種狀態——“人類逢魔”與“妖怪逢魔”,出發時狀態為“人類逢魔”。某些小路上可能被放置了“森羅結界”。在經過被放置結界的小路時,妖夢的人妖槽狀態將會發生變化——若經過這條小路前人妖槽狀態為“人類逢魔”,則經過後將變為“妖怪逢魔”;反之,若經過前狀態為“妖怪逢魔”,則經過後將變為“人類逢魔”。當且僅當人妖槽狀態為“妖怪逢魔”時,妖夢才可以收集到當前所在地點所蘊含的春度。
每個點的春度只能被收集一次。妖夢可以在圖上任意遊走,並可以選擇在任意一個地點停止收集。
妖夢希望收集到的總春度最大,但她並沒有學過OI,請你幫忙算出她最多能收集到多少春度。
因為並非所有人都具有結界內的常識,妖夢也提供了一份題意簡述 :
給定一個帶點權普通有向圖和一隻具有 \(0/1\) 狀態的妖夢,從 \(1\) 號節點出發,初始狀態為 \(0\) 。邊有 \(0/1\) 邊權,經過邊時狀態要異或上邊權。當前狀態為 \(1\) 時可取得所在點權,點權只能被取得一次。問在圖上隨意遊走可獲得的最大點權和。
Input
第一行四個整數 \(n\) , \(m\) ,表示圖由 \(n\) 個點, \(m\) 條邊構成。
接下來一行有 \(n\) 個整數 \(s_i\) ,表示\(i\)號節點蘊含 \(s_i\) 的春度。
接下來 \(m\) 行每行 \(3\) 個整數 \(u_i\) , \(v_i\) , \(w_i\) ,表示有一條從 \(u_i\) 到 \(v_i\) 的有向邊,若 \(w_i = 1\) ,則表示該小路上被放置了森羅結界,若 \(w_i = 0\) ,則表示未被放置。
Output
輸出一行一個整數,表示妖夢能收集到的最大總春度。
Sample 1
Sample 1 Input
5 6 99 82 44 35 3 1 2 1 2 3 0 3 4 1 4 5 0 2 4 1 3 5 1
Sample 1 Output
126
Sample 1 Explanation
路徑為 \(1\) -> \(2\) -> \(3\) ,可獲得 \(0 \times 99 + 1 \times 82 + 1 \times 44=126\) 點春度。
Sample 2
Sample 2 Input
9 10 9 9 8 2 4 4 3 5 3 1 2 0 2 3 1 3 2 0 3 4 0 4 5 1 5 6 0 6 4 1 2 5 0 7 8 1 9 8 1
Sample 2 Output
25
Sample 2 Explanation
路徑為 \(1\) -> \(2\) -> \(3\) -> \(2\) -> \(5\) -> \(6\) ,可以獲得 $0 \times 9 + 0 \times 9 + 1 \times 8 + 1 \times 9 + 1 \times 4 + 1 \times 4= 25 $ 點春度。
Sample 3
見
sample
目錄下spring3.in/.ans
。該樣例是一個無環圖。
Sample 4
見
sample
目錄下spring4.in/.ans
。Constraints
對於30%的資料,保證圖中無環。
對於另外20%的資料,保證圖隨機生成。
對於100%的資料, \(2 \le N \le 5 * 10^5\) , \(1 \le M \le 10^6\) , \(0 \le s_i \le 10^9\) , \(1 \le u_i,v_i \le N\) , \(w_i \in \{ 0,1 \}\) 。
Hints
由於幻想鄉不受常識束縛,不保證不出現重邊和自環,不保證圖連通。
輸入量較大,請使用較為快速的讀入方式。
保證時限在std用時的2倍左右。std沒有卡常,請放心食用。
Source
sun123zxy
Fun Facts
- 森羅結界 - 東方妖妖夢
- 人妖槽 - 東方永夜抄
- 題目名neta的是妖妖夢一面的卷首語。
Solution
無環圖
DAG上dp就好了。設狀態 \(f[u][0/1]\) 為到達點 \(u\) 時狀態為 \(0/1\) 可收集到的最大春度,若 \(f[u][t]\) 可達,有
其中 \(\mathrm{val}[u]\) 是點 \(u\) 的權值, \((v,w) \in \mathrm{pre}_u\) 表示 \(u\) 在DAG上的前驅邊, \(\otimes\) 代表異或。
答案即 \(\max_{u \in G} \max(f[u][0],f[u][1])\) 。
普通圖
普通圖有環,環上的狀態轉移方程相互依賴,無法dp。
根據部分分的提示,考慮縮點。
不妨先看所有強連通分量都只是簡單環的情況。
環套DAG
為了方便描述,我們定義如下兩種描述:
- 奇環:環上所有邊權異或和為 \(1\) 的環。
- 偶環:環上所有邊權異或和為 \(0\) 的環。
容易發現奇環上可以通過繞一圈的方式回到原點,使狀態發生改變。也就是說,不論從進出位置和初始狀態如何,一個奇環總可以輸出任意的 \(0\) 或 \(1\) 。而如果在奇環上繞兩圈,就可以取得環上所有點的春度。所以直接縮點處理即可。
那麼偶環如何處理呢?
首先,若進入偶環的的位置(入點)確定,無論怎樣在偶環上繞圈,到達環上某點(出點)時的狀態總是唯一確定的。
進一步的,偶環上的點可根據到達該點時的狀態被分為兩組。組與組之間在環上交錯排列,所有邊權為 \(1\) 的邊都是都是一個間隔。若入點和出點在同一組內,則狀態不會發生變化;反之則狀態改變。這啟發我們將偶環縮成兩個點來處理,每一個點代表一個組。
考慮春度的獲取。如果進入時狀態為 \(0\) ,那麼和入點在同一組內的點上的春度都無法取得(因為經過該點時狀態始終為 \(0\) ),而在不同組的點上的春度能夠取得(因為經過該點時狀態始終為 \(1\) );反之,若進入時狀態為 \(1\) ,那麼和入點在同一組的點上的春度可以取得,在不同組的不能取得。
縮點後做一些討論就可以了。
強連通分量
在環上我們已經發現——奇環可以特殊處理,而偶環內的點可以被分成兩組。強連通分量是否有與其相似的性質呢?
奇強連通分量
強連通分量無非是許多個環疊起來的連通塊。如果一個強連通分量包含一個或多個奇環(稱之為“奇強連通分量”),那麼該強連通分量同樣有奇環的性質——每個點都可以通過在奇環上繞圈獲得 \(0/1\) 兩種狀態,塊上所有點的春度都能取得。
實測發現隨機圖中出現偶強連通分量的概率極小,因此只處理奇強連通分量的演算法可以通過隨機圖資料。
偶強連通分量
剩下的問題已經很明確了——處理所含環全都是偶環的強連通分量(稱之為“偶強連通分量”)。
可以發現這一結論:無論如何在偶強連通分量中游走,只要入點和進入時的狀態確定,那麼每個點的狀態就唯一確定。於是偶強連通分量中的點也可以被分成兩組,好比環套DAG中的偶環。
易用反證法證明該性質:在一偶強連通分量中,假設點 \(u\) 到點 \(v\) 同時存在偶路徑 \(P\) 和奇路徑 \(Q\) 。那麼奇路徑 \(Q\) 必然與某條從 \(v\) 到 \(u\) 的奇路徑 \(R\) 共同組成了一個偶環(偶強連通分量中只有偶環且各點強連通)。則偶路徑 \(P\) 和奇路徑 \(R\) 構成奇環,與假設矛盾,故性質成立。
春度的獲取也與偶環相同。
判斷一個強連通分量是奇是偶,只需二分圖染色,取環上任意一個點作為起點DFS,如果能以不同的狀態到達某點,那該分量就是奇的,反之則是偶的。正確性比較顯然,證明在此略去。
實現
實現細節較多,建議縮點後重新建圖。
可以用4個節點分別代理兩個分組各自的入邊和出邊,算出到達該組狀態為 \(0/1\) 時連通塊內兩個組的點權對答案的貢獻。為了方便,實現時可以以邊數x2的代價把節點數壓縮到2個。
Code
/*
白銀之春 (spring) std
by sun123zxy
PS: If you got a runtime error, "-Wl,--stack=123456789"
*/
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
typedef long long ll;
ll Rd(){
ll ans=0;bool fh=0;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') fh=1; c=getchar();}
while(c>='0'&&c<='9') ans=ans*10+c-'0',c=getchar();
if(fh) ans=-ans;
return ans;
}
const ll INF=1E18;
const ll PTN=1E6+5,EDN=2E6+5;
ll N;
struct Edge{ll u,v;bool w;ll nxt;};
struct Graph{
Edge edge[EDN];
ll graM,last[PTN];
void GraphInit(){graM=0;for(ll i=0;i<PTN;i++) last[i]=0;}
void AddBscEdge(ll u,ll v,bool w){
edge[++graM]=(Edge){u,v,w,last[u]};
last[u]=graM;
}
void AddUnEdge(ll u,ll v,bool w){
AddBscEdge(v,u,w);
}
ll ptW[PTN][2]; //value Youmu can get when reaching the vertex with state 0/1
}G1,G2;
ll Id(ll cId,bool col){
return 2*cId-col;
}
ll bel[PTN],cN,rps[PTN]; //belong, number of components, representative vertax of the component
ll dfn[PTN],low[PTN],dN;
ll stk[PTN],tp;bool isI[PTN];
void Tarjan(ll u){
dfn[u]=low[u]=++dN;
stk[++tp]=u;isI[u]=1;
for(ll i=G1.last[u];i!=0;i=G1.edge[i].nxt){
ll v=G1.edge[i].v;
if(isI[v]){
low[u]=min(low[u],dfn[v]);
}else if(!dfn[v]){
Tarjan(v);
low[u]=min(low[u],low[v]);
}
}
if(dfn[u]==low[u]){
rps[++cN]=u;ll t;
do{
t=stk[tp--];
isI[t]=0;bel[t]=cN;
}while(t!=u);
}
}
bool cTyp[PTN]; //component type (0: even; 1: odd)
ll col[PTN];
void ColDFS(ll u,bool color,ll curC){
col[u]=color;
G2.ptW[Id(curC,color)][1]+=G1.ptW[u][1]; //calculate values for each group (even component)
for(ll i=G1.last[u];i!=0;i=G1.edge[i].nxt){
ll v=G1.edge[i].v;bool w=G1.edge[i].w;
if(bel[v]!=curC) continue;
if(col[v]==-1) ColDFS(v,color^w,curC);
else if((color^w)!=col[v]) cTyp[curC]=1; //odd component
}
}
void BuildG2(){
for(ll i=1;i<=G1.graM;i++){
ll u=G1.edge[i].u,v=G1.edge[i].v;bool w=G1.edge[i].w;
ll cU=bel[u],cV=bel[v];
if(!cU||!cV) continue; //edges Youmu can never reach
if(cU==cV) continue; //edges inside the component
ll myV=Id(cV,col[v]*(cTyp[cV]^1));
if(cTyp[cU]==1){
G2.AddUnEdge(Id(cU,0),myV,w);
G2.AddUnEdge(Id(cU,0),myV,w^1);
}else{
G2.AddUnEdge(Id(cU,col[u]),myV,w); //from this group
G2.AddUnEdge(Id(cU,col[u]^1),myV,w^1); //from the other group
}
}
}
ll f[PTN][2];
ll F(ll u,bool typ){
if(f[u][typ]!=-1) return f[u][typ];
f[u][typ]=-INF;
for(ll i=G2.last[u];i!=0;i=G2.edge[i].nxt){
ll v=G2.edge[i].v;bool w=G2.edge[i].w;
f[u][typ]=max(f[u][typ],G2.ptW[u][typ]+F(v,typ^w));
}
return f[u][typ];
}
ll ST=1;
void Solve(){
cN=0;dN=0;tp=0;for(ll i=1;i<=N;i++) dfn[i]=low[i]=0,bel[i]=0,isI[i]=0;
Tarjan(ST); //Only need to get components Youmu can reach
G2.GraphInit();
for(ll i=1;i<=N;i++) col[i]=-1;
for(ll i=1;i<=cN;i++) cTyp[i]=0,ColDFS(rps[i],0,i);
for(ll i=1;i<=cN;i++){
if(cTyp[i]==1){ //odd component
G2.ptW[Id(i,0)][0]=G2.ptW[Id(i,0)][1]+=G2.ptW[Id(i,1)][1]; //an odd component enjoys all the values
G2.ptW[Id(i,1)][0]=G2.ptW[Id(i,1)][1]=0; //abandon Id(i,1)
}else{ //even component
G2.ptW[Id(i,0)][0]=G2.ptW[Id(i,1)][1];
G2.ptW[Id(i,1)][0]=G2.ptW[Id(i,0)][1];
}
}
BuildG2();
for(ll i=1;i<=2*N;i++) f[i][0]=f[i][1]=-1;
ll myST=Id(bel[ST],col[ST]*(cTyp[bel[ST]]^1));
f[myST][0]=G2.ptW[myST][0];
ll ans=-INF;
for(ll i=1;i<=2*N;i++)
ans=max(ans,max(F(i,0),F(i,1)));
printf("%lld",ans);
}
int main(){
freopen("spring.in","r",stdin);
freopen("spring_std.out","w",stdout);
G1.GraphInit();
N=Rd();ll m=Rd();
for(ll u=1;u<=N;u++) G1.ptW[u][1]=Rd();
while(m--){
ll u=Rd(),v=Rd();bool w=Rd();
G1.AddBscEdge(u,v,w);
}
Solve();
return 0;
}
Omake
第一次出題,有紕漏請多多包涵。
快要交題時才發現一年前寫的std出鍋了,匆匆忙忙的重寫了一個,不知道有沒有新造出什麼bug。資料也造得比較匆忙,如果爆炸了請隨便辱罵出題人或者去他部落格上告訴他(
可以說這道題把二分圖擴充到了強連通有向圖上,不知道有沒有什麼更有趣的性質可以發掘。
後來做到幾道性質相似的題目,這裡列出來供參考: 垃圾撞題出題人
- CF1444C - Codeforces Round #680 - Team-Building (official solution)
- LOJ508 - LibreOJ NOI Round #1 - 失控的未來交通工具 (official solution)
思考背景怎樣與題目契合也是個挺有趣的過程。
感謝聽我亂扯idea的 TbYangZ 和 Waper ,以及嘗試叉掉std的兩位勇士 p9t6g 和 changruinian2020 。 雖然都失敗了
就這些吧。
——sun123zxy
Oct. 2019 初稿完成
Nov. 2020 最後更新
Next Phantasm...