树与图论的相关题目

1.树上求和.

因为是一棵树,从任意一点为根节点搜索都可以搜索完所有边。这里以1为根节点搜索。递归保存每个边的贡献次数。按贡献的次数从小到大排序,然后权值从n-1
到1相乘求和即可。 一个重要的知识点:U—V的边的贡献次数 = size(以V为边的子树结点数目)*(N-size)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
ll n,ans[N],cnt;
vector<int>g[N];
ll dfs(int u,int fa){
	ll sz=1;
	for(auto v:g[u])
		if(v!=fa) sz+=dfs(v,u);
	ans[cnt++]=sz*(n-sz);
	return sz;
}
int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1,0);
	sort(ans,ans+cnt);
	ll res=0;
	for(int i=n-1;i>0;i--) //res[0]是保存的以最后一个结点为起点的边的次数 显然没有为0 所以 i到1就够了 
		 res+=ans[n-i]*i;
		cout<<res<<endl;
	return 0;
} 

2.求树的直径

题目传送门:POJ1985模板题
下面上代码。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
using namespace std;
typedef long long ll;
#define mst(a) memset(a,0,sizeof a)
const int N=4e4+5;
struct edge{
	int to,nt,w;
}e[2*N];
int cnt,n,vis[N],h[N],d[N],m;//cnt记录边数,n顶点数,vis[i]标记结点i是否访问,h[i]头结点,d[i]保存到起点的最大距离 
void add(int u,int v,int w){
	e[cnt].to=v;
	e[cnt].w=w;
	e[cnt].nt=h[u];
	h[u]=cnt++;
}
void bfs(int u){
	mst(vis),mst(d);
	queue<int>q;
	q.push(u);
	while(q.size()){
		u=q.front();q.pop();vis[u]=1;
		for(int i=h[u];i;i=e[i].nt)
		{
			int v=e[i].to,w=e[i].w;
			if(!vis[v])
			{
				d[v]=d[u]+w;
				vis[v]=1;
				q.push(v);
			}
		}
	} 
}
int main(){
		ios::sync_with_stdio(false),cin.tie(0);
		cin>>n>>m; 
		cnt=1;//从1开始 
		mst(h);
		for(int i=1;i<=m;i++)
		{
			int u,v,w;
			char c;
			cin>>u>>v>>w>>c;
			add(u,v,w);
			add(v,u,w);
		}
		bfs(1);
		int mx=0,p;
		for(int i=1;i<=n;i++) ///这里是为了找到树的直径的一个端点B 
			if(d[i]>mx)
			{
				mx=d[i];
				p=i;// p为端点B 
			}
		bfs(p); //将端点B作为起点开始BFS 
		mx=0;
		for(int i=1;i<=n;i++){///找到另一个端点。 
			if(d[i]>mx)
			{
				mx=d[i];
				p=i; ///此时的端点A为直径的另一个端点。 
			}
		}
		cout<<mx<<endl;
	return 0;
} 

3.求树的多源最长路。

题目传送门:HDU2196

思路:利用树的直径和BFS(或者DFS),这里我用双BFS求直径的两个端点。然后对每个结点取较大值。
原理:树的一个结点的最长路的终点一定是树直径的某一个端点。这里简单证明下:任取一点u ,假设u的最长路终点为v,
情况1:若u在树的直径上,则v必为直径的端点.
情况2:若u不在树的直径上,用反证法,假设v不是直径的端点,u----v的路径与最长路p-----q的交点为c,因为
d(u,c)+d(c,v)>d(u,c)+d(c,p) 所以 d(c,v)>d(c,p)
所以 d(c,v)+d(c,q)>d(c,p)+d(c,q)=d(p,q) 即d(v,q)>d(p,q)与d(p,q)最大矛盾,所以v一定是直径的端点.
下面上代码。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mst(a) memset(a,0,sizeof a)
const int N=1e4+5;
struct edge{
	int to,nt,w;
}e[2*N];
int cnt,n,vis[N],h[N],d[N],pre[N];//cnt记录边数,n顶点数,vis[i]标记结点i是否访问,h[i]头结点,d[i]保存到起点的最大距离 
void add(int u,int v,int w){
	e[cnt].to=v;
	e[cnt].w=w;
	e[cnt].nt=h[u];
	h[u]=cnt++;
}
void bfs(int u){
	mst(vis),mst(d);
	queue<int>q;
	q.push(u);
	while(q.size()){
		u=q.front();q.pop();vis[u]=1;
		for(int i=h[u];i;i=e[i].nt)
		{
			int v=e[i].to,w=e[i].w;
			if(!vis[v])
			{
				d[v]=d[u]+w;
				vis[v]=1;
				q.push(v);
			}
		}
	} 
}
int main(){
	while(cin>>n){
		cnt=1;//从1开始 
		mst(h);
		for(int i=2;i<=n;i++)
		{
			int v,w;
			cin>>v>>w;
			add(i,v,w);
			add(v,i,w);
		}
		bfs(1);
		int mx=0,p;
		for(int i=1;i<=n;i++) ///这里是为了找到树的直径的一个端点B 
			if(d[i]>mx)
			{
				mx=d[i];
				p=i;// p为端点B 
			}
		bfs(p); //将端点B作为起点开始BFS 
		mx=0;
		for(int i=1;i<=n;i++){///找到另一个端点。 
			if(d[i]>mx)
			{
				mx=d[i];
				p=i; ///此时的端点A为直径的另一个端点。 
			}
		 pre[i]=d[i];  //pre[i]表示 结点i到 直径的一个端点B的最大距离 
		}
		bfs(p);//求每个结点到直径的端点A的最大距离。 
		for(int i=1;i<=n;i++)
			printf("%d\n",max(pre[i],d[i])); //此时的d[i]表示结点i到树直径的另一个端点A的最大距离 
	}   ///因为结点i的最大路径的终点肯定为树的直径的某一个端点。所以可以用max(pre[i],d[i]) 求 
	return 0;
} 

求树的割点.

题目传送门:POJ1144模板题

割点和割边。
割点的两个定理:定理1若树T的根结点s为割点,当且仅当s有两个及其以上的子结点. (这几个部分会分成几个子树,也就够成了两个及其以上的连通分量,若只有一个子结点,去掉这个结点,只会有一个子树,也就是一个连通分量,所以不是割点。
定理2:若树T的非根结点s为割点,当且仅当s存在至少一个子结点v及其后代都没有回退边能连回到s的祖先. (反证:若所有子结点都能通过回退边连回s的根节点,则去掉s后。S的子树仍与原来的树连通,仍然为一个连通块。
若存在一个,则该子结点及其后代能成为一个连通块,就分成了两个及其以上的连通块。
编程判断方法: 割点: 非根结点:low[v]>=num[u]
根结点:u==1&&child>=2
割边:low[v]>num[u] (u的子结点v最多只能到v本身,不会到u及u的祖先.即u—v这条边是割边。

上代码。

#include<iostream>
#include<cstring>
#include<vector>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=105;
#define mst(a) memset(a,0,sizeof a) 
int low[N],num[N],dfn,n;//dfn记录递归顺序.
bool vis[N];//标记是否为割点
vector<int> g[N];
void dfs(int u,int fa){
	low[u]=num[u]=++dfn;//初始值
	//printf("u=%d,fa=%d\n",u,fa);
	int child=0;
	for(int i=0;i<g[u].size();i++)
	{
		int v=g[u][i];
		if(!num[v]) //若v没有访问过 
		{
			child++;
			dfs(v,u);
			low[u]=min(low[v],low[u]);//用后代的返回值更新父结点的low值 
			//printf("low[%d]=%d,low[%d]=%d\n",u,low[u],v,low[v]);
			if(low[v]>=num[u]&&u!=1)//如果不是根节点且存在一个子结点没有回退边到u的祖先 
				vis[u]=1;
		}
		else if(num[v]<num[u]&&v!=fa) //处理回退边 即v能连接到u的祖先&&v是u的子结点 
		low[u]=min(low[u],num[v]);//说明u及其后代能通过回退边到u的祖先,更新low[u]
	 }
	 if(u==1&&child>=2) //如果是根结点则要求子结点数大于等于2 
	 	vis[u]=1; 
}
int main(){
	int u,v,ans;
		while(cin>>n&&n){
			mst(vis),mst(low),mst(num),dfn=ans=0; //初始化 
			for(int i=1;i<=n;i++) g[i].clear(); //初始化 
			 while(cin>>u&&u)
			 {
			 		while((getchar())!='\n')
			 		{
			 			 cin>>v;
			 			 g[u].push_back(v);
			 			 g[v].push_back(u);
 					}
			 }
			 dfs(1,0);
			 for(int i=1;i<=n;i++) ans+=vis[i];
			 cout<<ans<<endl;
		}
		return 0; 
}