题意:
就是有一个图,问你如果遍历所有的点,哪些边可以不走。
思考:
其实题意就是删除那个边以后,这个图还是联通的,所以很容易联想到求割边。
求割点模板题:割点。
求缩点模板题:缩点。
求割点例题:电力。
求边的双连通分量例题:冗余路径。
求点的双连通分量例题:矿场搭建。
代码:
求割边:
int T,n,m,k;
int ans;
int va[N];
int dfn[N],low[N],deep;
vector<int > e[N];
void dfs(int now,int p)
{
dfn[now] = low[now] = ++deep;
for(auto spot:e[now])
{
if(dfn[spot]==0)
{
dfs(spot,now);
low[now] = min(low[now],low[spot]);
if(low[spot]>dfn[now]) ans++;
}
else if(spot!=p) low[now] = min(low[now],dfn[spot]);
}
}
signed main()
{
IOS;
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
e[a].pb(b);
e[b].pb(a);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0) dfs(i,1);
}
cout<<m-ans;
return 0;
}
求割点:
int T,n,m,k;
int va[N];
int dfn[N],low[N],deep;
set<int > anw;
vector<int > e[N];
void dfs(int now,int root)
{
int res = 0;
dfn[now] = low[now] = ++deep;
for(auto spot:e[now])
{
if(dfn[spot]==0)
{
res++;
dfs(spot,root);
low[now] = min(low[now],low[spot]);
if(low[spot]>=dfn[now]&&now!=root) anw.insert(now);
}
else low[now] = min(low[now],dfn[spot]);
}
if(now==root&&res>=2) anw.insert(now);
}
signed main()
{
IOS;
cin>>n>>m;
while(m--)
{
int a,b;
cin>>a>>b;
e[a].pb(b);
e[b].pb(a);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0) dfs(i,i);
}
cout<<anw.size()<<"\n";
while(anw.size())
{
cout<<*anw.begin()<<" ";
anw.erase(*anw.begin());
}
return 0;
}
求缩点:
int n,m;
int maxn;
int map[N];
int dfn[N],low[N],vis[N],deep;
int col[N],cnt[N],sum;
int h[N],e[N],ne[N],idx;
int in[N];
int dist[N];
vector<int > v[N];
stack<int > s;
queue<int > q;
void dfs(int now);
void topsort();
void insert(int a,int b);
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>map[i];
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
v[a].push_back(b);
}
for(int i=1;i<=n;i++)
if(dfn[i]==0) dfs(i);
memset(h,-1,sizeof(h));
for(int i=1;i<=n;i++)
{
int a = i;
for(int j=0;j<v[i].size();j++)
{
int b = v[i][j];
if(col[a]!=col[b])
{
insert(col[a],col[b]);
in[col[b]]++;
}
}
}
topsort();
for(int i=1;i<=sum;i++)
maxn = max(maxn,dist[i]);
cout<<maxn;
return 0;
}
void topsort()
{
for(int i=1;i<=sum;i++)
{
dist[i] = cnt[i];
if(in[i]==0) q.push(i);
}
while(q.size())
{
auto now = q.front();
q.pop();
for(int i=h[now];i!=-1;i=ne[i])
{
int spot = e[i];
if(dist[spot]<dist[now]+cnt[spot])
{
dist[spot] = dist[now]+cnt[spot];
}
in[spot]--;
if(in[spot]==0)
{
q.push(spot);
}
}
}
}
void dfs(int now)
{
dfn[now] = low[now] = ++deep;
s.push(now);
vis[now] = 1;
for(int i=0;i<v[now].size();i++)
{
int spot = v[now][i];
if(dfn[spot]==0)
{
dfs(spot);
low[now] = min(low[now],low[spot]);
}
else if(vis[spot]) low[now] = min(low[now],dfn[spot]); //这里用low[spot]和dfn[spot]意义一样
}
if(dfn[now]==low[now])
{
col[now] = ++sum;
while(1)
{
int x = s.top();
s.pop();
vis[x] = 0;
col[x] = sum;
cnt[sum] += map[x];
if(x==now) break;
}
}
}
void insert(int a,int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
电力:
题意:
就是给你一个无向图,让你求出删除图的一个点之后,连通块最多可以是多少。
思考:
先看看一共有多少连通块,然后对于每个连通块看看删去哪个点更好,怎么看呢,就是看这个now点,会当做割点多少次,也就是删去now之后下面会有多少个联通块,最后再看看now是不是根节点,如果不是还能加上now上面的点组成的连通块。
代码:
#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 2e5+10,M = 2e6+10,K = 2e3+10;
int T,n,m,k;
int va[N];
int dfn[N],low[N],deep;
int ans,sum;
vector<int > e[N];
void dfs(int now,int root)
{
int cnt = 0;
dfn[now] = low[now] = ++deep;
for(auto spot:e[now])
{
if(dfn[spot]==0)
{
dfs(spot,root);
low[now] = min(low[now],low[spot]);
if(low[spot]>=dfn[now]) cnt++; //也就是删去now这个点,这些spot是被分割开的
}
else low[now] = min(low[now],dfn[spot]);
}
if(now!=root) cnt++; //如果不是根,那么now上面的点也会产生一个连通块
ans = max(ans,cnt); //最多的割点
}
void init()
{
ans = sum = deep = 0;
for(int i=0;i<=n;i++)
{
e[i].clear();
dfn[i] = low[i] = 0;
}
}
signed main()
{
IOS;
while(cin>>n>>m,n||m)
{
init();
while(m--)
{
int a,b;
cin>>a>>b;
e[a].pb(b);
e[b].pb(a);
}
for(int i=0;i<n;i++)
{
if(!dfn[i])
{
sum++;
dfs(i,i);
}
}
cout<<ans+sum-1<<"\n"; //-1是因为其中一个连通块是变成ans了
}
return 0;
}
冗余路径:
题意:
就是给你一个无向图,然后让你求出最少要加多少条边使得,这个图的任意两点都有两条不相交的路径。
思考:
其实就是对图进行的双连通分量进行缩点,最后看看有多少叶子节点,答案就是(叶子节点个数+1)/2,也就是叶子节点都要和别人连一条边,所以看看度为1的就行了。
其实边的双连通就是这个割边的左边和右边分别是两个联通块,最后缩完点也是一个树。
代码:
#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 2e5+10,M = 2e6+10,K = 2e3+10;
int T,n,m,k;
int va[N];
int dfn[N],low[N],deep,col[N],sum;
int in[N],out[N];
int h[N],e[N],ne[N],idx;
int bridge[N];
stack<int > s;
void add(int a,int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void dfs(int now,int p)
{
s.push(now);
dfn[now] = low[now] = ++deep;
for(int i=h[now];i!=-1;i=ne[i])
{
int spot = e[i];
if(!dfn[spot])
{
dfs(spot,i);
low[now] = min(low[now],low[spot]);
if(dfn[now]<low[spot]) bridge[i] = 1; //这个是桥
}
else if(i!=(p^1)) low[now] = min(low[now],dfn[spot]); //如果不是反向边,为什么不能用spot!=p呢?因为是可以有重边的,重边是看作不同的边的,所以不能仅仅看父亲点是谁,所以不能用vector存图,用邻接表会很方便.
}
if(dfn[now]==low[now])
{
col[now] = ++sum;
while(1)
{
int x = s.top();
s.pop();
col[x] = sum;
if(x==now) break;
}
}
}
signed main()
{
IOS;
cin>>n>>m;
mem(h,-1);
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
add(a,b);add(b,a);
}
dfs(1,-1);
for(int a=1;a<=n;a++)
{
for(int i=h[a];i!=-1;i=ne[i])
{
int b = e[i];
if(col[a]!=col[b])
{
in[col[b]]++;
out[col[a]]++;
}
}
}
int ans = 0;
for(int i=1;i<=sum;i++)
{
if(in[i]==1&&out[i]==1) ans++; //由于是双向边,所以叶子节点的出度和入度都会为1
}
cout<<(ans+1)/2;
return 0;
}
矿场搭建
题意:
就是给你一个无向图,然后任何一点都可能坍塌,问你最少要把多少点弄成逃生点,可以满足不论哪个点坍塌,任何点的位置的人都可以通过一个逃生点逃跑。
思考:
其实就是对点的双连通缩点之后,如果一个连通图里面没有割点,那么就要在其中设置两个逃生点(当然如果只有1个点的话那就设置一个)。如果有1个割点,那么在里面设置一个就行了,因为就算这个坍塌了,里面的人可以通过割点跑到另一个分量里面。
这里点的双连通就是,一个点是可以多个联通块的点,就是一个联通块包括了其中的割点。然后缩完点之后其实就是一个树。
代码:
#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define ull unsigned long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 2010,M = 2010;
int T,n,m,k;
int va[N];
int dfn[N],low[N],deep,sum;
stack<int > s;
vector<int > v[N];
vector<int > e[N];
void tarjan(int now,int root)
{
s.push(now);
dfn[now] = low[now] = ++deep;
if(now==root&&e[now].size()==0) //如果这个连通块仅仅有一个点
{
sum++;
v[sum].pb(now);
return ;
}
int cnt = 0;
for(auto spot:e[now])
{
if(!dfn[spot])
{
tarjan(spot,root);
low[now] = min(low[now],low[spot]);
if(dfn[now]<=low[spot])
{
cnt++;
if(now!=root||cnt>=2) va[now] = 1; //如果是割点
v[++sum].pb(now); //但是也要把now放进来
while(1)
{
int x = s.top();
s.pop();
v[sum].pb(x);
if(x==spot) break; //注意是到spot
}
}
}
else low[now] = min(low[now],dfn[spot]);
}
}
void init(int x)
{
while(s.size()) s.pop();
n = deep = sum = 0;
for(int i=0;i<=x;i++)
{
dfn[i] = low[i] = va[i] = 0;
e[i].clear();v[i].clear();
}
}
signed main()
{
IOS;
while(cin>>m&&m)
{
init(2e3);
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
n = max(n,max(a,b));
e[a].pb(b);e[b].pb(a);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) tarjan(i,i);
}
ull sum1 = 0,sum2 = 1;
for(int i=1;i<=sum;i++)
{
int cnt = 0,siz = v[i].size();
for(auto t:v[i]) if(va[t]) cnt++; //割点个数
if(cnt==0) sum1 += min(2ll,siz),sum2 *= max(1ll,siz*(siz-1)/2); //记得只有一个点的时候特判
else if(cnt==1) sum1 += 1,sum2 *= (siz-1); //对于叶子节点就放一个就可以了,对于cnt>=2的肯定能往cnt==1的跑
}
cout<<"Case "<<++T<<": "<<sum1<<" "<<sum2<<"\n";
}
return 0;
}
总结:
多多积累经验呀。