1. 程式人生 > 實用技巧 >網路攻擊

網路攻擊

Description:

Yixght是名為SzqNetwork(SN)的公司經理。 現在,她非常擔心,因為她剛剛收到一個壞訊息,這表明SN的業務競爭對手DxtNetwork(DN)打算攻擊SN的網路。 更不幸的是,SN的原始網路非常薄弱,我們只能將其視為一棵樹。 形式上,SN網路中有 NN 個節點, N−1N−1 個雙向通道連線這些節點,並且始終存在從任何節點到另一個節點的路由路徑。 為了保護網路免受攻擊,Yixght在某些節點之間建立了 MM 個新的雙向通道。
作為DN的最佳黑客,您可以準確地破壞兩個通道,一個破壞原始網路,另一個破壞M個新渠道。 現在,您的上司想知道您可以採用多少種方式將SN網路劃分為至少兩個部分。

Input Format:

輸入檔案的第一行包含兩個整數:$ N,M $ 。 代表節點數和新通道數。
\(N−1\)行表示SN原始網路中的通道,每對 \(a,b\)表示在節點 \(a\) 和節點 \(b\) 之間已經存在一個通道。
接下來的 \(M\) 行代表網路中的新新增通道,每對 \(a,b\) 表示節點 \(a\) 和節點 \(b\) 之間會新增一個新通道。

Output Format:

輸出一個整數。將網路劃分為至少兩個部分的方式的數量。

Sample Input:


 4 1
 1 2
 2 3
 1 4
 3 4

Sample Output:


 3

Hint:

  • 20%的資料 \(1 \leq N,M \leq 10\)
  • 40%的資料 \(1 \leq N,M \leq 1000\)
  • 100%的資料 \(1 \leq N,M \leq 100,000\)

Solution:

很容易想到樹上邊差分

對每條新加入的邊\((x,y)\),差分陣列\(s\)便\(s[x]++,s[y]++,s[lca(x,y)]-=2\),最後\(dfs\)一遍統計出每個點有多少條新加的邊經過,用\(d\)陣列維護。
\(d[i]=0\) 則刪去這一條邊與新加的任意一條邊皆可將\(i\)這個點及其子樹分離出來,\(ans+=M\)
\(d[i]=1\) 則這個點與一條新邊相連,刪去原來的邊與這條新邊也能將\(i\)與其子樹分出,\(ans++\)
\(d[i] \geq 2\) 則這個點與\(\geq 3\)條邊相連,無論怎麼刪都不行。
還有一點,根節點\(1\)在最後總會是0,但\(ans\)不能直接加\(M\),因為若\(1\)與其他節點相連則會多加,還需特判一下。

程式碼:

#include<bits/stdc++.h>
using namespace std;
const int N=500000;
typedef long long ll;
ll n,m,x,y,ans,xx,yy,f1,f2;
ll to[N];
ll nextn[N];
ll h[N];
ll deg[N];
ll f[N][20];
ll s[N];
ll d[N];
bool b[N];
void dfs(ll x,ll anc,ll dep){
	b[x]=1;
	f[x][0]=anc;
	deg[x]=dep;
	for(ll i=1;i<20;i++)f[x][i]=f[f[x][i-1]][i-1];
	for(ll i=h[x];i;i=nextn[i]){
		ll y=to[i];
		if(b[y])continue;
		if(x==1)f1++;
		dfs(y,x,dep+1);
	}
}
ll lca(ll x,ll y){
	if(deg[x]>deg[y])swap(x,y);
	for(ll i=19;i>=0;i--)if(deg[f[y][i]]>=deg[x])y=f[y][i];
	if(x==y)return x;
	for(ll i=19;i>=0;i--)if(f[x][i]!=f[y][i]){
		x=f[x][i];
		y=f[y][i];
	}
	return f[x][0];
}
ll dfs1(int x,int anc){
	d[x]=s[x];
	for(int i=h[x];i;i=nextn[i]){
		int y=to[i];
		if(y==anc)continue;
		d[x]+=dfs1(y,x);
	}
	return d[x];
}
int main(){
	scanf("%d%d",&n,&m);
	for(ll i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		to[2*i-1]=y;
		nextn[2*i-1]=h[x];
		h[x]=2*i-1;
		to[2*i]=x;
		nextn[2*i]=h[y];
		h[y]=2*i;
	}
	dfs(1,1,1);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		s[x]++;
		s[y]++;
		s[lca(x,y)]-=2;
		if(x==1||y==1)f2++;
	}
	dfs1(1,0);
	for(ll i=2;i<=n;i++){
		if(!d[i])ans+=m;
		if(d[i]==1)ans++;
	}
	if(f1+f2==1)ans+=m;
	if(f1==1&&f2==1)ans++;
	printf("%lld",ans);
}

然而,還有一個優化: 打錯lca還能過時想到的

對於每新加入的邊\((x,y)\)\(s\)陣列僅\(s[x]++,s[y]++\)

在最後統計出的\(d\)陣列便可與之前一樣算\(ans\)且少了根結點的特判。

比如這張圖:

(藍色邊為新加邊)
原先的\(s\)\(d\)陣列的值如下:
陣列 \(1\) \(2\) \(3\) \(4\) \(5\) \(6\)
\(s\) \(-3\) \(0\) \(0\) \(1\) \(1\) \(1\)
\(d\) \(0\) \(2\) \(0\) \(1\) \(1\) \(1\)
那按這種方法的\(s\)\(d\)陣列的值如下:
陣列 \(1\) \(2\) \(3\) \(4\) \(5\) \(6\)
\(s\) \(1\) \(0\) \(0\) \(1\) \(1\) \(1\)
\(d\) \(3\) \(2\) \(0\) \(1\) \(1\) \(1\)
可以看出,除\(1\)節點外,新的\(d\)陣列的\(0\)的個數與\(1\)的個數似乎與之前一樣
確實,只要是 子樹與該點 沒有在新邊上的 葉子節點的\(d\)值都將是0,\(ans\)照樣\(+M\)
\(d[i]=1\)則蘊涵\(s[i]=1\),所以\(i\)與新增一邊相鄰,與之前一樣,\(ans++\)
對於\(1\)節點,無論如何都不會是\(0\)\(1\),除非\(M=0\),這種情況直接排除。

於是程式碼如下,相對快很多:

#include<bits/stdc++.h>
using namespace std;
const int N=500000;
typedef long long ll;
ll n,m,x,y,ans,xx,yy,ii,jj;
ll to[N];
ll nextn[N];
ll h[N];
ll s[N];
ll d[N];
ll dfs(int x,int anc){
	d[x]=s[x];
	for(int i=h[x];i;i=nextn[i]){
		int y=to[i];
		if(y==anc)continue;
		d[x]+=dfs(y,x);
	}
	return d[x];
}
int main(){
	scanf("%d%d",&n,&m);
	for(ll i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		to[2*i-1]=y;
		nextn[2*i-1]=h[x];
		h[x]=2*i-1;
		to[2*i]=x;
		nextn[2*i]=h[y];
		h[y]=2*i;
	}
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		s[x]++;
		s[y]++;
	}
	dfs(1,0);
	for(ll i=1;i<=n;i++){
		if(!d[i])ans+=m;
		if(d[i]==1)ans++;
	}
	printf("%lld",ans);
}