牛客月赛12-华华和月月逛公园-(割点+割边+缩点+边的双连通+点的双连通)

I

题意:
就是有一个图,问你如果遍历所有的点,哪些边可以不走。

思考:
其实题意就是删除那个边以后,这个图还是联通的,所以很容易联想到求割边。
求割点模板题:割点
求缩点模板题:缩点
求割点例题:电力
求边的双连通分量例题:冗余路径
求点的双连通分量例题:矿场搭建

代码:

求割边:

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;
}

总结:
多多积累经验呀。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值