寒假集训专题七:图论入门

ESAY1:Stockbroker Grapevine

image

解题思路:
这道题我们先分析它的数据大小和数据间关系,我们可以得知这道题是小规模的寻找单一源头的最短路径问题,我们要使最长的一条路径长度最小。我们可以选择两种方法:遍历每一个源点运用Dijkstra算法(实现比较麻烦);floyd算法。通过图论学习,对于这种小规模的边数较多的图,求多源最短路径问题,我们一般选用floyd算法。
我们通过floyd更新距离矩阵然后遍历所有结点,将其作为原点,更新最远距离,再更新最终答案。最终将答案与我们一开始预设的无穷大作比较,如果相等说明所有点之间是不连通的,输出disjoint,否则输出结果。

#include<bits/stdc++.h>
using namespace std;

const int maxn = 110;
const int INF = 0x3f;
int dist[maxn][maxn];
int n;

void init()
{
	memset( dist, INF, sizeof( dist ) );
	for( int i = 1; i <= n; i++ )
	{
		dist[i][i] = 0;
	}
	return;
}

void floyd()
{
	for( int k = 1; k <= n; k++ )
	{
		for( int i = 1; i <= n; i++ )
		{
			for( int j = 1; j <= n; j++ )
			{
				dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
			}
		}
	}
	
	int pos = 0;
	int ans = INF;
	
	for( int i = 1; i <= n; i++ )
	{
		int temp = 0;
		for( int j = 1; j <= n; j++ )
		{
			if( i != j )
			{
				temp = max( temp, dist[i][j] );
			}
		}
		if( temp < ans )
		{
			pos = i;
			ans = temp;
		}
	}
	
	if( ans == INF )
	{
		printf( "disjoint\n" );
	}
	else
	{
		printf( "%d %d\n", pos, ans );
	}
	
}

int main()
{
	while( ~scanf( "%d", &n ) && ( n != 0 ) )
	{
		init();
		for( int i = 1; i <= n; i++ )
		{
			int x;
			scanf( "%d", &x );
			for( int j = 1; j <= x; j++ )
			{
				int no, dis;
				scanf( "%d %d", &no, &dis );
				dist[i][no] = dis;
			}
		}
		
		floyd();
	}
}

ESAY2:树的直径

image

解题思路:
这道题使用树形dp是较好的解决方法,对于添加边,我们把图绘制下来,然后选取一个点,遍历它的子节点,只要它不是父节点,就递归更新子节点的状态,在更新节点状态之前,先更新答案,dp[u]为当前节点为根子树最大的直径,同理,答案可以更新为原答案和两子树最大直径和加一。最后更新该点最大直径状态为dp[v]+1。最后结果就是最开始选取点dp[u]。
也可以用dfs()找到一个点,然后再从这个点dfs()求出最远点,得到最大距离。

#include<bits/stdc++.h>
using namespace std;

const int maxn = 1e5+10;
vector<int> g[maxn];
int dis[maxn];
int ans = 0;

void dp( int u, int fa )
{
	for( auto& v : g[u] )
	{
		if( v == fa ) continue;
		dp( v, u );
		ans = max( ans, dis[u] + dis[v] + 1 );
		dis[u] = max( dis[u], dis[v] + 1 );
	}
}

int main()
{
	int n;
	cin >> n;
	memset( dis, 0, sizeof(int) * ( n + 1 ) );
	for( int i = 1; i < n; i++ )
	{
		int u, v;
		cin >> u >> v;
		g[u].push_back(v);
		g[v].push_back(u);	
	}
	
	dp( 1, 0 );
	cout << ans << endl;
	return 0;
	 
}

//#include<bits/stdc++.h>
//using namespace std;
//
//const int maxn = 1e5+6;
//int n;
//int ans = 0, farNode = 0;
//vector<int> graph[maxn];
//
//void dfs( int cur_node, int fathernode, int d )
//{
//	if( d > ans )
//	{
//		ans = d;
//		farNode = cur_node;
//	}
//	for( int v : graph[cur_node] )
//	{
//		if( v != fathernode )
//		{
//			dfs( v, cur_node, d + 1 );	
//		}  
//	}
//}
//
//
//int main()
//{
//	cin >> n;
//	
//	for( int i = 1; i < n; i++ )
//	{
//		int x, y;
//		cin >> x >> y;
//		graph[x].push_back(y);
//		graph[y].push_back(x);	
//	}
//	
//	dfs( 1, -1, 0 );
//	
//	ans = 0;
//	
//	dfs( farNode, -1, 0 );
//	
//	cout << ans << "\n";
//	
//	return 0;
//	
//}

MEDIUM1: Invitation Cards

image

解题思路:
这道题数据范围很大,且是单源问题,所以我们选择用Dijkstra算法,这道题主要的核心在于如何反向建图,我们先用dijkstra算法进行正向建图,之后我们要得到每个点到源点的花费的总和最小值,我们可以想一下,在一个有向图里面,我们把边反向之后,从一个源点发出的图是不是就变成会聚在源点的图,出于这个考虑,我们在初始化图的时候选择用mp来存储正向的初始边,同时用remp存储反向的初始边,然后在对于mp的dijkstra函数中用dist来存储正向结果,同理remp也要存储。
dijkstra函数中利用优先队列维护能到达的最短距离,将当前起始点推入队列,然后对于邻接边中能更新最短距离的点推入队列,如果当前距离已更新过,跳过。
最后只要从2号点开始遍历,将其在正反两图的和加入到答案中,就是最终答案了。

#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <queue>
#include <algorithm>
#include <functional>
#define ll long long
using namespace std;

const int maxn = 1e6+7;
const ll INF = 0x3f3f3f3f3f3f3f3f;

vector<pair<int,int>> mp[maxn];
vector<pair<int,int>> remp[maxn];
ll dist_first[maxn], dist_second[maxn];
int P,Q,T;
ll ans;

void dijkstra( int start, int n, long long dist[], vector<pair<int,int>> mp[] )
{
	priority_queue< pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>> > pq;
	memset( dist, INF, sizeof( ll ) * ( P + 1 ) ) ;
	dist[start] = 0;
	pq.push( { start, 0 } );
	
	while( !pq.empty() )
	{
		int cur = pq.top().first;
		long long cur_dist = pq.top().second;
		pq.pop();
		
		if( cur_dist > dist[cur] ) continue;
		
		for( auto& edge : mp[cur] )
		{
			int to = edge.first;
			ll weight = edge.second;
			if( dist[cur] + weight < dist[to] )
			{
				dist[to] = dist[cur] + weight;
				pq.push( { to, dist[to] } );
			}
		}	
	}	
}

int main()
{
	ios::sync_with_stdio(false); // 加速输入输出
    cin.tie(nullptr);
	cin >> T;
	while( T-- )
	{
		cin >> P >> Q;
		
		for( int i = 1; i <= P; i++ )
		{
			mp[i].clear();
			remp[i].clear();
		}
		
		while( Q-- )
		{
			int u, v;
			ll w;
			cin >> u >> v >> w;
			mp[u].push_back( { v, w } );
			remp[v].push_back( { u, w } );
		}
		
		dijkstra( 1, P, dist_first, mp );
		
		dijkstra( 1, P, dist_second, remp );
		
		ll total_cost = 0;
		for( int i = 2; i <= P; i++ )
		{
			total_cost += dist_first[i] + dist_second[i];
		}
		
		cout << total_cost << "\n";
		
	}
	return 0;
}

MEDIUM2:战略游戏

P2016 战略游戏

题目背景

Bob 喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。

题目描述

他要建立一个古城堡,城堡中的路形成一棵无根树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能瞭望到所有的路。

注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被瞭望到。

请你编一程序,给定一树,帮 Bob 计算出他需要放置最少的士兵。

输入格式

第一行一个整数 \(n\),表示树中结点的数目。

第二行至第 \(n+1\) 行,每行描述每个结点信息,依次为:一个整数 \(i\),代表该结点标号,一个自然数 \(k\),代表后面有 \(k\) 条无向边与结点 \(i\) 相连。接下来 \(k\) 个整数,分别是每条边的另一个结点标号 \(r_1,r_2,\cdots,r_k\),表示 \(i\) 与这些点间各有一条无向边相连。

对于一个 \(n\) 个结点的树,结点标号在 \(0\)\(n-1\) 之间,在输入数据中每条边只出现一次。保证输入是一棵树。

输出格式

输出文件仅包含一个整数,为所求的最少的士兵数目。

输入输出样例 #1

输入 #1

4
0 1 1
1 2 2 3
2 0
3 0

输出 #1

1

说明/提示

数据规模与约定

对于全部的测试点,保证 \(1 \leq n \leq 1500\)
解题思路:
树形dp的常用场景求树的最小点覆盖,我们先用邻接表将树存储下来,然后来定义一下树的状态dp[x][0]为不选中该点,dp[x][1]为选中该点。
从0号点开始遍历0点的每一个邻接点,如果邻接点与父节点相同,跳过。如果是在选用该点情况下无论是否选用邻接点都是可以的,加上最小值便是,如果不选用该点,那子节点应该都选择,所以求其和。
最后返回答案dp[0][0]和dp[0][1]的最小值就是最终答案。

#include<bits/stdc++.h>
using namespace std;

const int maxn = 1510;
vector<int> g[maxn];
int ans[maxn][2];

void dp( int x, int fa )
{
	ans[x][1] = 1,//放士兵 
	ans[x][0] = 0;//不放士兵 
	for( auto& y : g[x] )
	{
		if( y == fa ) continue;
		dp( y, x );
		ans[x][1] += min( ans[y][0], ans[y][1] );
		ans[x][0] += ans[y][1];
	}
}

int main()
{
	int n;
	cin >> n;
	
	for( int i = 0; i < n; i++ )
	{
		int u, v, t;
		cin >> u >> t;
		while( t-- )
		{
			cin >> v;
			g[u].push_back( v );
			g[v].push_back( u );
		}	
	}
	
	dp( 0, -1 );
	
	cout << min( ans[0][1], ans[0][0] ) << endl;
	
	return 0;
}

学习总结:

一共三种最短路问题的算法:
floyd算法适用于多源最短路径问题,时空复杂度都高,所以比较适合小规模图问题。
dijkstra算法适合正权图的单元最短路径问题,用了优先队列来优化时间复杂度,对于稀疏图,效率较高。
spfa算法用来解决dijkstra算法解决不了的负权边问题。

//floyd算法
for( int k = 1; k <= n; k++ )
{
	for( int i = 1; i <= n; i++ )
	{
		for( int j = 1; j <= n; j++ )
		{
			dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
		}
	}
}
  //拓展一下floyd算法的最小环问题
	for( int k = 1; k <= N; k++ )
	{
		//更新答案
		for( int i = 1; i < k; i++ )
		{
			for( int j = i + 1; j < k; j++ )
			{
				ans = min( ans, cost[i][j] + w[i][k] + w[k][j] );	
			}	
		}
		
		//更新最短距离矩阵 
		for( int i = 1; i <= N; i++ )
		{
			for( int j = 1; j <= N; j++ )
			{
				cost[i][j] = min( cost[i][j], cost[i][k] + cost[k][j] );
			}
		} 
	}

//dijkstra算法 有点类似dfs
void dijkstra( int start, int n, long long dist[], vector<pair<int,int>> mp[] )
{
	priority_queue< pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>> > pq;
	memset( dist, INF, sizeof( ll ) * ( P + 1 ) ) ;
	dist[start] = 0;
	pq.push( { start, 0 } );
	
	while( !pq.empty() )
	{
		int cur = pq.top().first;
		long long cur_dist = pq.top().second;
		pq.pop();
		
		if( cur_dist > dist[cur] ) continue;
		
		for( auto& edge : mp[cur] )
		{
			int to = edge.first;
			ll weight = edge.second;
			if( dist[cur] + weight < dist[to] )
			{
				dist[to] = dist[cur] + weight;
				pq.push( { to, dist[to] } );
			}
		}	
	}	
}

//spfa没咋用过
......

//树形dp
void dp( int u, int fa )
{
	for( auto& v : g[u] )
	{
		if( v == fa ) continue;
		dp( v, u );
		ans = max( ans, dis[u] + dis[v] + 1 );
		dis[u] = max( dis[u], dis[v] + 1 );
	}
}
posted @ 2025-02-19 16:04  yeqa  阅读(47)  评论(0)    收藏  举报