網路攻擊
阿新 • • 發佈:2020-08-01
- 原題:網路攻擊
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);
}