1.图的相关定义
注意,图中重要的两个权值!做题时搞清楚,是对 “点权” 的处理还是对 “边权” 的处理。
1.1 点权
需要单独开一个数组来记录点权,weight[] 。
1.2 边权
邻接矩阵、邻接表存的就是边权。
2.图的存储
2.1邻接矩阵
节点数量在 以下
存储边权:
int G[MAXN][MAXN]
存储点权:
int weight[MAXN]
2.2邻接表
存储边权:
struct node{
int v; // 终点
int w; // 边权
}
vector<node> Adj[MAXN]; // 邻接表
存储点权:
int weight[MAXN]
3.图的遍历
3.0 图的模拟
3.0.1 图上路径的模拟
核心:按照给定路径(或要求),在图上 模拟走出路径,但一般限制条件较多,理清楚要求即可。并不涉及到 DFS/BFS。
典例 A1150 Travelling Salesman Problem- 图的模拟
题目大意:给出 m 对边信息(建图),给出 k 条路径判断是 TS、TS simple 路径。
TS simple 路径:从一个点出发,遍历到图上所有节点,回到初始点,的简单 路径。则有判断条件如下:
- 途中有节点不可达 Not a TS cycle.
- 没有遍历到所有的点(用 set 统计) Not a TS cycle.
- 起点终点不一 Not a TS cycle.
- 简单路径:在满足上述条件下, 路径的长度 > k+1(说明必有点重复访问)
代码:
#include<cstdio>
#include<algorithm>
#include<vector>
#include<set>
#define MAXN 10010
using namespace std;
int n,m;
int G[MAXN][MAXN];
int ans = 0x3f3f3f3f,ansi;
int a,b,c;
int main(){
scanf("%d%d",&n,&m) ;
for(int i = 1;i <= m;++i){
scanf("%d%d%d",&a,&b,&c);
G[a][b] = G[b][a] = c;
}
int k,s;
scanf("%d",&s) ;
for(int i = 1;i <= s;++i){
int k;
scanf("%d",&k);
vector<int> v(k);
set<int> st;
scanf("%d",&v[0]);
int dist = 0;
int flag = 1; // 是否可达?
for(int j = 1;j < k;++j){
scanf("%d",&v[j]);
st.insert(v[j]);
if(G[v[j-1]][v[j]] == 0) flag = 0;
dist += G[v[j-1]][v[j]];
}
// 是否可达
if(flag == 0){
printf("Path %d: NA (Not a TS cycle)\n",i);
}else if(st.size() != n || v[0] != v[k-1]){ // 没有访问到所有的点,或者没有访问到原点
printf("Path %d: %d (Not a TS cycle)\n",i,dist);
}else { // 构成 TS 回路
if(k > n+1){ // 必定有重复
printf("Path %d: %d (TS cycle)\n",i,dist);
} else printf("Path %d: %d (TS simple cycle)\n",i,dist);
if(dist < ans){
ans = dist;
ansi = i;
}
}
}
printf("Shortest Dist(%d) = %d\n",ansi,ans);
return 0;
}
3.0.2 图的边信息
核心:根本不涉及到图的结构,只要保存下图的边,遍历所有的边进行判断即可。
典例 A1154 Vertex Coloring
题目大意:给出⼀个图(先给出所有边,后给出每个点的颜⾊),问是否满⾜:所有的边的两个点的
颜⾊不相同
分析:把 所有边存起来,把所有点的颜⾊存起来(存的过程中放⼊set统计颜⾊个数),枚举所有边,
检查是否每条边的两点个颜⾊是否相同,若全不相同,则输出颜⾊个数,否则输出No~
代码:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
vector<PII> vec;
int n,m;
int a,b;
int main() {
scanf("%d%d",&n,&m);
for(int i = 0; i < m; ++i) {
scanf("%d%d",&a,&b);
vec.push_back(make_pair(a,b));
}
scanf("%d",&b);
while(b--) {
vector<int> v(n); // 保存每个的颜色
set<int> st;
for(int i = 0; i < n; ++i) {
scanf("%d",&v[i]);
st.insert(v[i]);
}
int ok = 1;
for(int i = 0; i < vec.size(); ++i) {
if(v[vec[i].first] == v[vec[i].second]){
ok = 0;
printf("No\n");break;;
}
}
if(ok) printf("%d-coloring\n",st.size());
}
return 0;
}
3.1 DFS遍历
* DFS 遍历题型 总结
就三件事:
1.建图
2.设计DFS
3.遍历图
DFS 内部实现 也就三件事:
0.维护访问数组 vis[ i ]
1.终点判断(一般没有)
2.修改标记 [+ 具体操作(计数、更新)]
3.所有可走点 DFS
const int MAXV = 1000; // 最大节点数
const int INF = 0x7fffffff;
邻接矩阵版:
int n; // 节点数
// 1. 建图
int G[MAXV][MAXV]; // 邻接表
bool vis[MAXV] = {false}; // 访问标记
//2.设计 DFS
void DFS(int u,int depth){
// 1.终点判断
// 2.操作
vis[u] = true;
//3.所有可走点 DFS
for(int i = 0;i < n;++i){
// 没访问过,且可达
if(vis[i] == false && G[u][i] != INF){
DFS(i,depth+1);
}
}
}
//3.遍历图
void DFSTrave(){
for(int i = 0; i <n;++i){
if(vis[i] == false){
DFS(i,1);
}
}
}
邻接表版:
int n; // 节点数
// 1. 建图
struct node{
int v;
int w;
}
vector<node> Adj[MAXV]; // 邻接表
bool vis[MAXV] = {false}; // 访问标记
//2.设计 DFS
void DFS(int u,int depth){
// 1.终点判断
// 2.操作
vis[u] = true;
//3.所有可走点 DFS
for(int i = 0;i < Adj[u].size();++i){
// 没访问过,且可达
if(vis[ Adj[u][i] ] == false){
DFS(Adj[u][i],depth+1);
}
}
}
//3.遍历图
void DFSTrave(){
for(int i = 0; i <n;++i){
if(vis[i] == false){
DFS(i,1);
}
}
}
典例 Head of a Gang
题意:给出若干人之间的通话长度,这些通话将他们分为若干组。每个组的总边权 = 通话长度之和,每个人的点权 = 该人参与通话长度之和。给定阈值 K,只要一个组的总边权大于K,且人数大于2,即为犯罪团伙“Gang”,组内点权最大的视为头目。
输出:犯罪团伙的个数,按字典序从小到大输出每个团伙的头目姓名,和成员人数。
分析:本题的处理设计到 边权 和 点权。
需要搞清楚很重要一个问题:
DFS到底是 遍历 所有的 点 还是所有的 边 ??
显然,本题要求所有的边权之和,是要遍历所有的边,即点是可以重复访问的(否则无法访问所有的边)。
遍历点:vis[ i ] 数组标记该点是否访问,修改标记代表已访问。
遍历边:G[ i ][ j ] 的权重是否合法,修改边权代表已访问。
本题要点:
1.是遍历所有的 “边” 而不是 “点。通过修改 G[][] 邻接表的边权使边无法重复访问;
2.map 适用于 string - int 之间转换的保存,并不是用来代替 字符串哈希。
3.DFS 过程中用 “引用” 保存 成员数,头目,总边权,在 DFS 外进行判断。
代码:
#include<cstdio>
#include<vector>
#include<string>
#include<map>
#include<iostream>
#define MAXN 1010
using namespace std;
// 邻接矩阵
int G[MAXN][MAXN];
//记录点权
int weight[MAXN] ;
// 访问数组
bool vis[MAXN] = {false};
map<string,int> name2int;
map<int,string> int2name;
map<string,int> gang; // 记录头目对应的 gang 的人数
int n,k;
string name1,name2;
int Time;
int number; // 用输入的顺序作为编号
int change(string& str){
if(name2int.find(str) != name2int.end()){
// 已经出现过,则直接返回
return name2int[str];
}else{
name2int[str] = number++;
return name2int[str];
}
}
// 2. DFS
void DFS(int u,int& num,int& head,int& total){
// 1.终点判断
// 2.操作
// 置为访问
// cout<<"now node: "<<int2name[u]<<endl;
if(vis[u] == false) num++;
vis[u] = true;
if(weight[u] > weight[head]) head = u;
// 3.所有可走点 dfs
for(int i = 0;i < number;++i){
if( G[u][i] > 0){
// 没访问过且可达
total += G[u][i];
G[u][i] = 0;
G[i][u] = 0;
// cout<<"now total:"<<total<<endl;
DFS(i,num,head,total);
}
}
}
// 3.遍历图
void DFSTrave(){
for(int i = 0;i < number;++i){
if(int2name.find(i) != int2name.end()){
int num = 0,head = -1,total = 0;
DFS(i,num,head,total);
// cout<<"head:"<<int2name[head]<<"total:"<<total<<endl;
if(num > 2 && total > k){
gang[int2name[head]] = num;
}
}
}
}
int main(){
scanf("%d%d",&n,&k);
// 1.建图
for(int i = 0;i < n;++i){
// 输入一个人,还要判断他是否出现过
cin>>name1>>name2>>Time;
int n1 = change(name1);
int n2 = change(name2);
int2name[n1] = name1;
int2name[n2] = name2;
// 更新点权
weight[n1] += Time;
weight[n2] += Time;
// 更新边权
G[n1][n2] += Time;
G[n2][n1] += Time;
}
// 遍历图
DFSTrave();
cout<<gang.size()<<endl;
for(map<string,int>::iterator it = gang.begin(); it != gang.end();++it){
cout<<it->first<<" "<<it->second<<endl;
}
return 0;
}
典例 Battle over cities - 并查集
题意:给出一张无向图,删除某点后,求需要几条边使得剩下的点连通?
思路:连通块的个数 - 1;
连通块:DFS;并查集
方法二:并查集
由于后续需要删除点,所以不能读入时建立并查集,应当先保存下边的信息,然后每一次动态建立。
#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
#define MAXN 10010
using namespace std;
int father[MAXN];
vector<int> Adj[MAXN];
bool vis[MAXN] = {false};
int n,m,k;
// 初始化
void init() {
for(int i = 1; i <= n; ++i) {
father[i] = i;
}
}
// 查
int find_father(int x) {
int a = x;
while(x != father[x]) {
x = father[x];
}
// 路径压缩
while(a != x) {
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
// 并
void merge(int a,int b) {
int a1 = find_father(a);
int b1 = find_father(b);
if(a1!=b1) father[a1] = b1;
}
int f(int no) {
// 初始化
init();
//建立并查集
for(int i = 1;i<=n;++i){
if(i == no ) continue;
for(int j = 0;j < Adj[i].size();++j){
if(Adj[i][j] != no) merge(i,Adj[i][j]);
}
}
// 统计连通块个数
int cnt = 0;
for(int i = 1;i <= n;++i){
if(i == no) continue;
if(i == father[i]) ++cnt;
}
return cnt;
}
int main() {
scanf("%d%d%d",&n,&m,&k);
// 初始化
int a,b;
//由于,要删去某个点,所以要先保存下边信息,然后再建立并查集
for(int i = 0; i < m; ++i) {
scanf("%d%d",&a,&b);
Adj[a].push_back(b);
Adj[b].push_back(a);
}
int tmp;
for(int querry = 0; querry < k; ++querry) {
scanf("%d",&tmp);
printf("%d\n",f(tmp)-1);
}
return 0;
}
3.2 BFS遍历
* BFS 遍历题型 总结
1.涉及到层数时,优先选用BFS。
2.用并查集统计连通块的个数,节省时间。
BFS内部实现 三件事:
0.维护入队标记 inq[ i ]
1.创建队列,初始节点入队
2.队不空时循环,出队一个元素
3.所有相邻可走点入队,修改标记。
邻接矩阵版:
int n;
int G[MAXV][MAXV];
bool inq[MAXV] = {false}; // 是否入队标记
void BFS(int u){
// 1.建队列 ,初始节点入队
queue<int> qu;
qu.push(u);
inq[u] = true;
// 队不空时循环
while(!qu.empty()){
// 出队一个元素
int now = qu.front();
qu.pop();
// 所有可走点入队
for(int i = 0;i <n;++i){
if(inq[i] == false && G[u][i] != INF){
qu.push(i);
inq[i] = true;
}
}
}
}
void BFSTrave(){
for(int i = 0;i < n;++i){
if(inq[i] == false){
BFS(i);
}
}
}
邻接表版:
/// 邻接表 版
vector<int> Adj[MAXV];
int n;
bool inq[MAXV] = {false}; // 是否入队标记
void BFS(int u) {
// 1.建队列 ,初始节点入队
queue<int> qu;
qu.push(u);
inq[u] = true;
// 队不空时循环
while(!qu.empty()) {
// 出队一个元素
int now = qu.front();
qu.pop();
// 所有可走点入队
for(int i = 0; i < Adj[u].size(); ++i) {
if(inq[ Adj[u][i] ] == false ) {
qu.push(i);
inq[i] = true;
}
}
}
}
void BFSTrave(){
for(int i = 0;i < n;++i){
if(inq[i] == false){
BFS(i);
}
}
}
添加层号:
struct Node{
int v; // 节点编号
int layer; // 层号
};
vector<Node> Adj[MAXV];
int n;
bool inq[MAXV] = {false}; // 是否入队标记
void BFS(int s) {
// 1.建队列 ,初始节点入队
queue<Node> qu;
Node start;
start.v = s;
start.layer = 1;
qu.push(start);
inq[start.v] = true;
// 队不空时循环
while(!qu.empty()) {
// 出队一个元素
Node topNode = qu.front();
qu.pop();
int u = topNode.v;
// 所有可走点入队
for(int i = 0; i < Adj[u].size(); ++i) {
Node next = Adj[u][i];
next.layer = topNode.layer + 1;
if(inq[ next.v ] == false ) {
qu.push(next);
inq[next.v] = true;
}
}
}
}
典例 The deepest root
题意:给出 个顶点和 条边,问:它们能否构成一棵树?如果能,从中选出树根,使得树的高度最大。输出所有的树根。
思路:
1.求连通块的个数:并查集;
2.求树的深度:BFS。
#include<cstdio>
#include<algorithm>
#include<vector>
#include<queue>
#include<cstring>
#define MAXN 10010
using namespace std;
struct Node{
int v; // 终点
int layer; // 层数
};
vector<Node> G[MAXN];
bool inq[MAXN] = {false};
int n;
int father[MAXN];
void init(){
for(int i = 1;i <= n;++i) father[i] = i;
}
int findFather(int x){
int a = x;
while(x != father[x]) x = father[x];
while(a != x){
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
void Union(int a,int b){
int fa = findFather(a);
int fb = findFather(b);
if(fa != fb) father[fa] = fb;
}
int BFS(int u){
int maxLayer = 1;
queue<Node> qu;
Node start;
start.v = u;
start.layer = 1;
inq[u] = true;
qu.push(start);
Node topNode;
while(!qu.empty()){
topNode = qu.front();
int u = topNode.v;
qu.pop();
Node tmp;
for(int i = 0;i < G[u].size();++i){
if(inq[G[u][i].v ] == false){
tmp.v = G[u][i].v;
tmp.layer = topNode.layer + 1;
maxLayer = max(maxLayer,tmp.layer);
qu.push(tmp);
inq[tmp.v] = true;
}
}
}
return maxLayer;
}
int main(){
scanf("%d",&n);
int a,b;Node t;
init();
for(int i = 0;i < n-1;++i){
scanf("%d%d",&a,&b);
t.v = b;
G[a].push_back(t);
t.v = a;
G[b].push_back(t);
Union(a,b);
}
// 用并查集统计连通块的个数
int cnt = 0;
for(int i = 1;i <= n;++i) if(i == father[i]) ++cnt;
if(cnt > 1) {
printf("Error: %d components",cnt);
return 0;
}
// 从每个点 BFS,记录下最大值
int maxDepth = 0;
vector<int> ans;
for(int i = 1;i <= n;++i){
memset(inq,false,sizeof(inq));
if(inq[i] == false) {
int tmp = BFS(i);
if(tmp == maxDepth){
ans.push_back(i);
}else if(tmp > maxDepth){
maxDepth = tmp;
ans.clear();
ans.push_back(i);
}
}
}
for(int i= 0;i < ans.size();++i){
printf("%d\n",ans[i]);
}
return 0;
}
典例 7-10 Save James Bond - 遍历
题目大意:地图大小100*100(-50-+50),007站在一个直径为15的岛上,需要踩着鳄鱼跳到岸边。给出鳄鱼的位置和007的跳跃距离,问能否跳到岸边?
分析:由于只需要回答是否,则判断所有可达的点中是否存在可以跳到岸边的点即可。
注意节点用struct 或 pair 存储。
代码:
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<vector>
#include<map>
#include<queue>
#include<cmath>
#define INF 0x3f3f3f3f
#define MAXN 1010
using namespace std;
typedef pair<int,int> PII;
int n,dist;
double calDist(const PII& a,const PII& b) {
return sqrt((a.first-b.first)*(a.first-b.first) + (a.second-b.second*(a.second-b.second)));
}
int a,b;
PII nodes[MAXN];
map<PII,bool> inq;
int main() {
scanf("%d%d",&n,&dist) ;
if(dist + 7.5 >= 50){
printf("Yes\n");return 0;
}
for(int i = 0; i< n; ++i) {
// scanf("%d%d",&a,&b);
scanf("%d%d",&nodes[i].first,&nodes[i].second);
}
// bfs 遍历寻找可达的点中能否上岸
int flag = 0;
// 初始节点入队
queue<PII> qu;
for(int i = 0; i< n; ++i) {
if(calDist(nodes[i],make_pair(0,0)) <= 7.5+dist) {
qu.push(nodes[i]);
inq[nodes[i]] = true;
}
}
while(!qu.empty()) {
PII top = qu.front();
qu.pop();
if(abs(top.first)+dist >= 50 || abs(top.second)+dist >= 50) {
flag = 1;
break;
}
for(int i = 0; i < n; ++i) {
if(!inq[nodes[i]] && calDist(top,nodes[i]) <= dist) {
qu.push(nodes[i]);
inq[nodes[i]] = true;
}
}
}
printf("%s\n",flag?"Yes":"No");
return 0;
}
3.3 最短路径
3.3.1 * Dijkstra 算法
求单源最短路径算法。
零、Dijkstra 考察内容总结
- Dijkstra 基础算法:初始化 + 三步;
- 记录路径:添加
- 添加第二尺度:再加一个数组保存特征,在 第三步的更新步骤中,当路径更小更新路径时,更新特征;当路径相等时,考虑是否更新特征。
一、Dijkstra 算法概述
0.设点的全集为 , 点集 ,表示已经求出 从起点到目标点的最短距离的那些点,初始时为空;设表示到的最短距离,并设;设记录最短路径上的前趋结点,初始设为自身。
1.每次从 中取出一个距离最近的点;
2.加入;
3.扩展 ; 考虑:它的加入,是否使到中的点的距离变小了,是则更新。
为了在初始时能选出起点,应使;为了获取路径,由于记录的是前趋结点,则从终点开始递归,使用后序思想,在递归到终点向上返回时,再输出。
代码如下:
邻接矩阵版:
/ Dijkstra算法三大步骤
// 0. 初始化,将 S0 到 所有点的距离设为 INF ; 将 d[s0] = 0;
// 1. n 次循环,一次从 V-S 中取出一个距 S0 最近的一个点,
// 2.将其加入 S
// 3.从刚加入的点扩展;判断刚加入的点,是否会更新 V-S 中的点到 S0 的距离。
// 注意:这样一定是保证 距离最短的,
// 记录路径:
// 使用 pre[] 数组记录 更新权值后 的结点的前趋
// 使用 DFS 在向上返回时输出
bool vis[MAXN] = {false}; // S 集合标记
int d[MAXN]; // 所有点到 S0 的最短距离
int n,m,s; // 顶点个数,边数,起点编号 // 节点个数
int pre[MAXN]; // 记录 S0 到 V 的最短路径 (显然只能记录一条路径)
// 输出路径
void DFS(int s,int v) { // 从终点开始递归,递归到底向上返回时输出,后序思想
if(v == s) {
printf("%d\n",pre[v]) ;
return;
}
DFS(s,pre[v]);
printf("%d\n",v) ;
}
// 邻接矩阵版 Dijkstra
int G[MAXN][MAXN];
void Dijkstra(int s) {
// 0.初始化,
fill(d,d+MAXN,INF);
for(int i = 0; i < n; ++i) pre[i] = i;
d[s] = 0;
// n 次循环,一次从 V-S 中取出一个距离 S0 最近的点,初始时 S 为空
for(int i = 0; i < n; ++i) {
// 从 V-S 中取出一个距离 S0 最近的点
int u = -1,MIN = INF;
for(int j = 0; j < n; ++j) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
// 将其加入 S 集合
if(u == -1) return ; // 说明没有点可达,即不连通
vis[u] = true;
// 扩展;判断刚加进来的点,是否会更新 V-S 中的点到 S0 的距离
for(int v = 0; v < n; ++v) {
if(vis[v] == false && G[u][v] != INF && d[u]+G[u][v] < d[v]) {
d[v] = d[u]+G[u][v];
pre[v] = u; // 记录路径
}
}
}
}
邻接表版:
PS:就只是在访问边的时候不一样,其他都一模一样。
// 邻接表版 Dijkstra
struct Node {
int v;
int dis; // dis 为边权
};
vector<Node> Adj[MAXN];
void Dijkstra(int s){
// 0.初始化 S(vis[]) ,d[]
fill(d,d+MAXN,INF);
d[s] = 0;
// 循环 n 次
for(int i = 0;i < n;++i){
// 从 V-S 中取出一个元素
int u = -1,MIN = INF;
for(int j = 0;j < n;++j){
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
// 将其加入 S 集合
if(u == -1) return ;
vis[u] = true;
// 3.扩展,判断是否更新
for(int j = 0; j < Adj[u].size();++j){
int v = Adj[u][j].v;
if(vis[v] == false && Adj[u][j].dis + d[u] < d[v]){
d[v] = Adj[u][j].dis + d[u];
pre[v] = u;
}
}
}
}
二、记录路径
见 一 中代码。
三、添加第二尺度
最短路径不止一条,需要通过第二尺度判断。
再加一个数组保存第二尺度(边权、点权、路径条数),在 第三步的扩展更新步骤中,当路径更小更新路径时,更新特征;当路径相等时,考虑是否更新特征。
首先,距离仍是第一要素,距离更新时,第二尺度随之更新;距离相等时,判断第二尺度是否需要更新。
1.给每条边再增加一个边权(比如花费)。要求最短路径时花费之和最小。
添加数组 cost[][] 记录新增的边权
c[i] 记录 到 i 的最小花费之和, c[s] = 0
// 1.选出距离 S0 最近的点
// 2.加入 S
// 3.扩展,判断更新
for(int v = 0;v < n;++v){
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
c[v] = c[u] + cost[u][v];
}
else if(d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]){
// 相等时加一个额外判断即可
c[v] = c[u] + cost[u][v];
}
}
}
2.添加点权
添加数组 weight[] 保存点权
w[i] 记录到 i 的最大点权之和, w[s] = weight[s]
// 1.选出距离 S0 最近的点
// 2.加入 S
// 3.扩展,判断更新
for(int v = 0;v < n;++v){
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
w[v] = w[u] + weight[v];
}
else if(d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]){
// 相等时加一个额外判断即可
w[v] = w[u] + weight[v];
}
}
}
3.添加路径条数
添加数组 num[i] 记录到 i 的最短路径条数
// 1.选出距离 S0 最近的点
// 2.加入 S
// 3.扩展,判断更新
for(int v = 0;v < n;++v){
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
num[v] = num[u]; // 继承 u 的路径条数
}
else if(d[u] + G[u][v] == d[v]){
// 相等时加一个额外判断即可
num[v] += num[u]; // 最短距离同时累加 num
}
}
}
典例 Emergency - Dijkstra
注意搞清楚题意是 有向图还是无向图!
一般来说,两城市之间的公路应该是无向图,除非特别说明。
题意:给出城市之间的无向图,每个城市有点权,求出到的最短路径条数,以及最大点权。
方法一:普通 Dijkstra 开两个数组 +
#include<cstdio>
#include<algorithm>
#define MAXN 600
#define INF 0x3fffffff
using namespace std;
int n,m,c1,c2;
int u,v,t;
int G[MAXN][MAXN];
int weight[MAXN];
int vis[MAXN] = {false};
int d[MAXN];
int num[MAXN];
int w[MAXN];
void Dijkstra(int s){
// 0.初始化
fill(d,d+MAXN,INF);
d[s] = 0;
num[s] = 1;
w[s] = weight[s];
// n 次循环
for(int i = 0;i < n;++i){
// 1.从 V-S 中选出距 S0 最近的
int u = -1,MIN = INF;
for(int j = 0;j < n;++j){
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
// 2. 加入 S ,
if(u == -1) return ;
vis[u] = true;
// 3. 扩展,判断 u 的加入是否 更新 V-S 的距离
// 实际上就是 扩展边 的过程
for(int v = 0;v < n;++v){
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v] ){
d[v] = d[u] + G[u][v];
num[v] = num[u];
w[v] = w[u] + weight[v];
}else if(d[u] + G[u][v] == d[v]){
num[v] += num[u];
if(w[u] + weight[v] > w[v]){
w[v] = w[u] + weight[v];
}
}
}
}
} // end of for n
}
int main(){
// 初始化
fill(G[0],G[0]+MAXN*MAXN,INF);
scanf("%d%d%d%d",&n,&m,&c1,&c2);
for(int i = 0;i < n;++i) scanf("%d",&weight[i]);
for(int i = 0;i < m;++i){
scanf("%d%d%d",&u,&v,&t);
G[u][v] = t;
G[v][u] = t;
}
Dijkstra(c1);
printf("%d %d",num[c2],w[c2]);
return 0;
}
3.3.2 * Dijkstra + DFS 算法
此套算法模板的
核心思想:
- 1.Dijkstra 只找出 “ 所有的” 最短路径(第一尺度);
- 2.DFS从所有的最短路径中找出最优路径(第二尺度)。
步骤:
1.Dijkstra
- 求出最短路径 d[] (第一标尺);
- 同时用 vector 数组 pre 存下所有最短路径,
2.DFS 从终点逆向递归
- 先 求出一条完整的路径 tmpPath;
- 遍历路径,记录下最优路径 Path.
第二标尺的判断:
- 边权,用 cost[][] 存,c[i] 表示起点到 i 的最小花费。
- 点权:用 weight[] 存,w[i] 表示起点到 i 的最大价值。
- 数目,用 num[] 存,可在 Dijkstra 中,也可在 DFS 终点。
Dijkstra找最短路径的方法相同,用 vector pre[] 记录下 v 所有的前趋结点。
重点在于 DFS 找最优路径。 从终点出发,一路DFS到起点,然后一起计算路径上的总权重,路径已经被记录在pre中了。
模板:
vector<int> tmpPath,Path;
void DFS(int v){
//递归终点
if(v == st){ // 如果到达起点
// 终点需要单独插入
tmpPath.push_back(v);
int value = 0;
计算value的值,点权或者边权。
if(value 优于 optvalue){
optvalue = value;
Path = tmpPath;
}
// 终点需要单独删除,因为 return 后的代码执行不到了
tmpPath.pop_back();
return ;
}
//递归式
tmpPath.push_back(v);
for(int i = 0;i < pre[v],size();++i){
DFS(pre[v][i]);
}
// 恢复环境
tmpPath.pop_back();
}
例如:
计算边权和点权的代码:
PS:路径条数的统计,可以使用普通Dijkstra,也可以在递归终点统计。
当题目简洁、只有一个第二尺度时,使用普通Dijkstra也会更简洁。
vector<int> tmpPath,Path;
void DFS(int v){
递归终点
//1.计算边权
if(v == st){ // 如果到达起点
int value = 0;
// 逆着计算,因为DFS是从终点逆向起点的
for(int i = tmpPath.size()-1;i ‘>’ 0;--i){
int id = tmpPath[i],idNext = tmpPath[i-1];
value += G[id][idNext];
}
if(value > maxValue){
maxValue = value;
Path = tmpPath;
}
}
递归式
。。。
//2.计算点权
if(v == st){ // 如果到达起点
int value = 0;
// 逆着计算,因为DFS是从终点逆向起点的
for(int i = tmpPath.size()-1;i ‘>=’ 0;--i){
int id = tmpPath[i];
value += weight[id];
}
if(value > maxValue){
}
}
}
模板:
int G[MAXN][MAXN];
bool vis[MAXN] = {false};
int d[MAXN];
int weight[MAXN];
vector<int> pre[MAXN];
int num[MAXN];
void Dijkstra(int s) {
// 0.初始化
fill(d,d+MAXN,INF);
d[s] = 0;
num[s] = 1;
// n 次循环
for(int i = 0; i < n; ++i) {
// 1. 从 V-S 中取出一个距离 S0 最近的点
int u = -1,MIN = INF;
for(int j = 0; j < n; ++j) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
// 2.加入 s
if(u == -1) return ;
vis[u] = true;
// 3 .从 u 扩展 V-S
for(int v = 0; v < n; ++v) {
if(vis[v] == false && G[u][v] != INF ) {
if(d[u] + G[u][v] < d[v]) {
第一标尺
pre[v].clear();
pre[v].push_back(u);
d[v] = d[u] + G[u][v];
第二标尺中的 num 可以放在这
num[v] = num[u];
} else if(d[u] + G[u][v] == d[v]) {
pre[v].push_back(u);
num[v] += num[u];
}
}
}
}
}
vector<int> tmpPath,Path; // 由于 tmpPath 是全局变量,所以需要回复环境的
int maxValue; // 记录下最大的点权或边权
// 从终点 (固定的) 开始逆向 DFS 至 起点(固定的),然后一起算路径上的总权值
void DFS(int s){
// 递归终点,当递归到起点时,结束
if(s == c1){
tmpPath.push_back(s);
// 计算权值 (点权)
计算第二标尺
int value = 0;
for(int i = tmpPath.size()-1;i >= 0 ;--i){
value += weight[tmpPath[i]];
}
if(value > maxValue){
maxValue = value;
Path = tmpPath;
}
// 恢复环境
tmpPath.pop_back();
}
// 递归式,所有可走点 DFS
//添加路径
tmpPath.push_back(s) ;
// 所有可走点 DFS
for(int i = 0;i < pre[s].size();++i){
DFS(pre[s][i]);
}
// 恢复环境
tmpPath.pop_back();
}
典例 Emergency - Dijkstra + DFS
方法二: Dijkstra + DFS
注意:是有向边还是无向边!!
#include<cstdio>
#include<algorithm>
#include<vector>
#define MAXN 1010
#define INF 0x3fffffff
using namespace std;
// Dijkstra + DFS 思想:
// Dijkstra 只记录下最短路径(第一标尺),但要记录下多条路径,就要用 vector
// 每次遇到更小的路径时需要清空 vector 路径,所以不需要初始化
// DFS 从终点开始向起点递归,寻找满足条件的最优路径(第二标尺),保存路径,递归到终点时,一起计算路径上的权值
int n,m,c1,c2;
int G[MAXN][MAXN];
bool vis[MAXN] = {false};
int d[MAXN];
int weight[MAXN];
vector<int> pre[MAXN];
int num[MAXN];
void Dijkstra(int s) {
// 0.初始化
fill(d,d+MAXN,INF);
d[s] = 0;
num[s] = 1;
// n 次循环
for(int i = 0; i < n; ++i) {
// 1. 从 V-S 中取出一个距离 S0 最近的点
int u = -1,MIN = INF;
for(int j = 0; j < n; ++j) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
// 2.加入 s
if(u == -1) return ;
vis[u] = true;
// 3 .从 u 扩展 V-S
for(int v = 0; v < n; ++v) {
if(vis[v] == false && G[u][v] != INF ) {
if(d[u] + G[u][v] < d[v]) {
pre[v].clear();
pre[v].push_back(u);
d[v] = d[u] + G[u][v];
num[v] = num[u];
} else if(d[u] + G[u][v] == d[v]) {
pre[v].push_back(u);
num[v] += num[u];
}
}
}
}
}
vector<int> tmpPath,Path; // 由于 tmpPath 是全局变量,所以需要回复环境的
int maxValue; // 记录下最大的点权或边权
// 从终点 (固定的) 开始逆向 DFS 至 起点(固定的),然后一起算路径上的总权值
void DFS(int s){
// 递归终点,当递归到起点时,结束
if(s == c1){
tmpPath.push_back(s);
// 计算权值 (点权)
int value = 0;
for(int i = tmpPath.size()-1;i >= 0 ;--i){
value += weight[tmpPath[i]];
}
if(value > maxValue){
maxValue = value;
Path = tmpPath;
}
// 恢复环境
tmpPath.pop_back();
}
// 递归式,所有可走点 DFS
//添加路径
tmpPath.push_back(s) ;
// 所有可走点 DFS
for(int i = 0;i < pre[s].size();++i){
DFS(pre[s][i]);
}
// 恢复环境
tmpPath.pop_back();
}
int main() {
fill(G[0],G[0]+MAXN*MAXN,INF);
scanf("%d%d%d%d",&n,&m,&c1,&c2);
for(int i = 0; i< n; ++i) {
scanf("%d",&weight[i]);
}
int u,v,tmp;
for(int i= 0; i <m; ++i) {
scanf("%d%d%d",&u,&v,&tmp);
G[u][v] = tmp;
G[v][u] = tmp;
}
Dijkstra(c1);
DFS(c2);
printf("%d %d",num[c2],maxValue);
return 0;
}
典例 Public Bike Management - 加油问题,第三标尺
题意:城市里有一些公共自行车站,每个车站的最大容量为一个偶数,如果该车站的自行车数量为,那么就称该车站处于完美状态。
如果一个车站的容量是满的或者空的,控制中心就要携带或者从路上搜集一定数量的自行车前往该车站,以使问题车站及沿途所有车站处于完美状态。现给出、车站数目、问题车展编号、无向边数以及边权,求一条从控制中心到的最短路径,输出需要携带的自行车数目、路径、需要携带回中心的数目。如果路径有多条,则选择携带最少的;如果仍有多条,则选择带回中心最少的。
注意“调整过程需要在前往问题车站的过程中就调整完毕,返程不调整。
分析:
1.调整的过程是动态的,类似加油问题,而不能单纯的算总和。
英文原文是读不出动态过程的。
2.本题加入了第三标尺,第一标尺是距离,第二标尺是需求need,第三标尺是剩余remain。
注意:第二标尺更新时,第三标尺无条件更新,不要说是 Dijkstra+DFS就忘了这一点。
加油问题的逻辑:
在DFS从终点回溯到起点,计算路径总权值时,设need表示需要从中心带的数目,remain表示当前剩余的可用来补给的数目。
// 递归终点
if(v == 0) {
tmpPath.push_back(v);
int need = 0,remain = 0;
for(int i = tmpPath.size()-2; i >= 0; --i) {
int id = tmpPath[i];
if(weight[id] >= cmax / 2) {
// 如果超过了一半,可以用来补给
remain += weight[id] - cmax / 2;
} else {
// 当前不够
if(remain > cmax/2-weight[id]) {
// 携带的够补给,那么就补给
remain -= cmax/2-weight[id];
} else {
// 不够,补给剩下的要从 PBMC 带
need += (cmax/2-weight[id])-remain;
remain = 0;
}
}
}
完整代码:
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cmath>
#define INF 0x3fffffff
#define MAXN 550
using namespace std;
int cmax,n,sp,m;
int weight[MAXN];
int G[MAXN][MAXN];
int d[MAXN];
bool vis[MAXN] = {false};
int w[MAXN];
vector<int> pre[MAXN];
void Dijkstra(int s) {
// 0.初始化
fill(d,d+MAXN,INF);
d[0] = 0;
// n 次循环
for(int i = 0; i <= n; ++i) {
// 1. 从 V-S 中取出最近的
int u = -1,MIN = INF;
for(int j = 0; j <= n; ++j) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
// 2.加入 S
if(u == -1) return ;
vis[u] = true;
//3.扩展 u
for(int v = 0; v <= n; ++v) {
if(vis[v] == false && G[u][v] != INF) {
if(d[u]+G[u][v] < d[v]) {
d[v] = d[u]+G[u][v];
pre[v].clear();
pre[v].push_back(u);
} else if( d[u]+G[u][v] == d[v]) {
pre[v].push_back(u);
}
}
}
}
}
vector<int> tmpPath,Path;
int minRemain = INF,minNeed = INF;
int bestValue;
int flag; // 0 是找最大值,1 是找最小值 (full)
void DFS(int v) {
// 递归终点
if(v == 0) {
tmpPath.push_back(v);
int need = 0,remain = 0;
for(int i = tmpPath.size()-2; i >= 0; --i) {
int id = tmpPath[i];
if(weight[id] >= cmax / 2) {
// 如果超过了一半,可以用来补给
remain += weight[id] - cmax / 2;
} else {
// 当前不够
if(remain > cmax/2-weight[id]) {
// 携带的够补给,那么就补给
remain -= cmax/2-weight[id];
} else {
// 不够,补给剩下的要从 PBMC 带
need += (cmax/2-weight[id])-remain;
remain = 0;
}
}
}
if(need < minNeed) {
minNeed = need;
Path = tmpPath;
minRemain = remain;
} else if(need == minNeed) {
if(remain < minRemain) {
minRemain = remain;
Path = tmpPath;
}
}
tmpPath.pop_back();
return ;
}
// 递归式,所有可走点 DFS
tmpPath.push_back(v);
for(int i = 0; i < pre[v].size(); ++i) {
DFS(pre[v][i]);
}
tmpPath.pop_back();
}
int main() {
fill(G[0],G[0]+MAXN*MAXN,INF);
scanf("%d%d%d%d",&cmax,&n,&sp,&m);
for(int i = 1; i <= n; ++i) {
scanf("%d",&weight[i]);
}
int si,sj,t;
for(int i = 0; i < m; ++i) {
scanf("%d%d%d",&si,&sj,&t);
G[si][sj] = G[sj][si] = t;
}
Dijkstra(0);
DFS(sp);
int len = Path.size();
printf("%d ",minNeed);
printf("0");
for(int i = len-2; i >= 0; --i) {
printf("->%d",Path[i]);
}
printf(" %d",minRemain);
return 0;
}
典例 Gas Station - 多次Dijkstra
题意:加油站选址需要使加油站与任何住宅之间的最小距离尽可能远;但是同时必须保证所有房屋都在其服务范围内。题目给定城市地图和加油站的几个候选地点,题目要求给出地点推荐。如果有多个解,则输出到所有房子的平均距离最小的那个。如果这样的解决方案仍然不是惟一的,则输出索引号最小的解决方案。
思路:分别求每一个加油站候选位置到居民区的距离,由于有多个点,所以需要多次 Dijkstra ,(最开始想的是Floyd)。并且,题目只要求距离,所以不用保存路径,也不用DFS求路径,只获得 数组即可。
第一标尺:距离;
第二标尺:最小距离最大;
第三标尺:平均距离最小。
注意点:居民点 与 gas station 的处理,读入字符串,将保存为,即跟着居民后面累加。使用 atoi
函数
代码:
#include<cstdio>
#include<algorithm>
#include<vector>
#include<string>
#include<cstring>
#define MAXN 1020
#define INF 0x3fffffff
using namespace std;
int n,m,k,ds;
int G [MAXN][MAXN];
int dist;
char p1[10],p2[10];
int a,b;
int N;
int d[MAXN];
bool vis[MAXN] = {false};
void Dijkstra(int s) {
// 0.初始化
fill(d,d+MAXN,INF);
fill(vis,vis+MAXN,false);
d[s] = 0;
// n 次循环
for(int i = 1; i <= N; ++i) {
// 1.从 V-S 中挑一个最近的
int u = -1,MIN = INF;
for(int j = 1; j <= N; ++j) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
// 2.加入 S
if(u == -1) return ;
vis[u] = true;
// 3.扩展 u
for(int v = 1; v <= N; ++v) {
if(vis[v] == false && G[u][v] != INF) {
if(d[u] + G[u][v] < d[v]) {
d[v] = d[u]+G[u][v];
}
}
}
}
}
double maxMinDis = 0;
double minAvgDis = INF;
int ans;
int main() {
scanf("%d%d%d%d",&n,&m,&k,&ds);
N = n+m;
fill(G[0],G[0]+MAXN*MAXN,INF);
for(int i = 0; i < k; ++i) {
scanf("%s%s",p1,p2);
scanf("%d",&dist);
if(p1[0] == 'G') {
a = n + atoi(p1+1);
} else a = atoi(p1);
if(p2[0] == 'G') {
b = n + atoi(p2+1);
} else b = atoi(p2);
// printf("a:%d b:%d\n",a,b);
G[a][b] = G[b][a] = dist;
}
// for(int i = 1;i <= N;++i){
// for(int j =1;j <= N;++j){
// printf("%d ",G[i][j]);
// }
// printf("\n");
// }
// 从 每个 Gas 出发,Dijkstra,求 最小距离和平均距离
for(int i = n+1; i <= N; ++i) {
int flag = 1;
Dijkstra(i);
// 求最小距离
// mindis 记录最小距离,totdis 记录总长度们用来求平均
double mindis = INF,totdis = 0;
for(int j = 1; j <= n; ++j) {
totdis += d[j];
if(d[j] > ds){
flag = 0;
break;
}
if(d[j] < mindis) {
mindis = d[j];
}
}
if(flag == 0) continue;
// 更新最小值
double avg = totdis / n;
if(mindis > maxMinDis) {
maxMinDis = mindis;
minAvgDis = avg;
ans = i;
} else if(mindis == maxMinDis) {
if(avg < minAvgDis){
minAvgDis = avg;
ans = i;
}
}
}
if(ans){
printf("G%d\n",ans-n);
printf("%.1lf %.1lf",maxMinDis,minAvgDis);
}else printf("No Solution");
return 0;
}
3.4拓扑排序
主要作用:判断有向无环图(DAG)。
步骤:
- 1.定义一个队列 ,将所有入度为
- 2.取出队首节点输出;删去所有从它出发的边,令这些边到达的点入度减 1.
- 3.反复进行 2. 操作,直到队列为空。
如果队列为空时,入过队的结点数恰好为 N,则拓扑排序成功,是有向无环图;否则排序失败,图中有环。
(只有入度为 0 的点才能入队,有节点没有入队,说明有环的存在)
由于需要记录结点的入度,需要额外建立一个数组
代码:
#include<cstdio>
#include<algorithm>
#include<vector>
#define MAXN 10010
using namespace std;
// topologicalSort 思想:
// 将 入度为 0 的点加入队列,然后删除他的边,将新的 入度为0的点加入
// 重复上述步骤,看最后 加入拓扑排序路径的点 是否是全部,是则为 DAG
vector G[MAXN];
int inDegree[MAXN]; // 保存每个点的入度
int n;
bool topologicalSort() {
int sum = 0; // 统计排序的数目
queue<int> qu;
// 初始时 入度 为 0 的点 入队
for(int i = 0; i < n; ++i) {
if(inDegree[i] == 0) {
qu.push(i);
}
}
while(!qu.empty()) {
int u = qu.front();
qu.pop();
++sum;
// 删除它的出边
for(int i = 0; i < G[u].size(); ++i) {
int v = G[u][i];
inDegree[v]--;
if(inDegree[v] == 0) {
qu.push(v);
}
}
G[u].clear(); // 如无必要,可不写
}
if(sum == n) return true;
else return false;
}
int main() {
...
return 0;
}
典例 Topological Order
题目:给出图,和几个序列,找出不是拓扑序列的序列。
输入:N,K表示点的个数和边的个数。K行,代表边。
跟着S,输入S个序列。
输出:不是拓扑序列的编号。
回顾拓扑排序的过程:
0.将初始入度为 0 的点加入队列,
1.出队一个元素,删除他的边,若该边指向的点入度为 0,则将其加入;
2.重复上述步骤直至队列为空,最后判断入过队的个数。
其中,可能有多个入度为 0 的点,其入队的先后顺序不同,可获得不同的拓扑序列。
而现在题目给出了一个具体的序列,需要我们判断是否是拓扑序列。那么问题转化为:
按照给定的序列进行入队操作,判断是否是合法(入队的元素入度为 0 )的入队操作。
即:按照给定序列,该入队的结点,其入度是否为 0 ?
代码:
#include<cstdio>
#include<algorithm>
#include<queue>
#define MAXN 1010
#define INF 0x3fffffff
using namespace std;
int n,k;
int G[MAXN][MAXN];
int a,b;
int s[MAXN];
int inDegree[MAXN];
int inDegree1[MAXN];
bool topologicalSort() {
for(int i = 1; i <= n; ++i) inDegree1[i] = inDegree[i];
// 按照给出序列进行操作
for(int i= 0; i < n; ++i) {
int u = s[i];
// 入度不为 0, 直接失败
if(inDegree1[u] != 0) return false;
// 否则将其入队,然后删除边
for(int j = 1; j <= n; ++j ) {
if(G[u][j] != INF) --inDegree1[j];
}
// printf("\ninDegree1:\n");
// for(int i = 0;i < n;++i) printf("%d ",inDegree1[i]);
// printf("\n");
}
return true;
}
vector<int> ans;
int main() {
fill(G[0],G[0]+MAXN*MAXN,INF);
scanf("%d%d",&n,&k);
for(int i = 0; i < k; ++i) {
scanf("%d%d",&a,&b);
G[a][b] = 1;
inDegree[b]++;
}
scanf("%d",&k);
for(int i = 0; i < k; ++i) {
for(int j = 0; j < n; ++j) {
scanf("%d",&s[j]);
}
if(topologicalSort() == false) ans.push_back(i);
}
for(int i = 0; i < ans.size(); ++i) {
printf("%d",ans[i]);
if(i < ans.size()-1) printf(" ");
}
return 0;
}
3.5 BellmanFord- SPFA
BellmanFord
步骤 :
- V-1 轮循环(V为节点数),每轮循环遍历所有的 边(故使用邻接表),进行松弛(更新)操作(与Dijkstra更新的部分一样)。
- 在做一轮松弛,如果有点被更新,则说明有负权环。
注意:前趋点的保存 pre[] 数组换用 set<int> pre[MAXN]。因为一个点可能会更新多次。
dfs 求第二标尺也是一模一样,只是要用 迭代器 it 遍历 set 。
解释:一点到另一点的最短路径最长为 v-1,故进行 v-1 轮循环。每次遍历所有的边进行松弛。
代码:
int n,m,c1,c2;
int weight[MAXN]; // 点权
struct edge {
int v;
int dist;
};
vector<edge> G[MAXN];
int d[MAXN];
set<int> pre[MAXN]; // 换用 set 存储,因为可能会有多次更新
/*
思想:重复 n-1 轮:遍历所有的边,进行松弛操作
*/
bool bellman(int s) {
// 0. 初始化
fill(d,d+MAXN,INF);
d[s] = 0;
// 重复 n-1 轮
for(int i = 0; i <n-1; ++i) {
// 遍历所有的边,进行松弛操作 : !!! 取每个点的每条边
for(int u = 0; u < n; ++u) {
for(int j = 0; j < G[u].size(); ++j) {
int v = G[u][j].v; // 边的目标点
int dis = G[u][j].dist; // 边的长度
// 松弛
if(d[u] + dis < d[v]) {
d[v] = d[u] + dis;
pre[v].clear();
pre[v].insert(u);
} else if(d[u] + dis == d[v]) pre[v].insert(u);
}
}
}
// 判断负权环 : 再做一轮,如果有更新,则说明有负权环
for(int u = 0; u < n; ++u) {
for(int j = 0; j < G[u].size(); ++j) {
int v = G[u][j].v; // 边的目标点
int dis = G[u][j].dist; // 边的长度
// 松弛
if(d[u] + dis < d[v]) {
return false;
}
}
}
return true;
}
vector<int> tmpPath,Path;
int maxValue;
int num;
void dfs(int x) { // 与 Dijkstra 一样 的
if(x == c1) {
tmpPath.push_back(x);
// 能够到达终点,路径条数 + 1
num++;
int value = 0;
for(int i = 0; i < tmpPath.size(); ++i) value += weight[tmpPath[i]];
if(value > maxValue) {
maxValue = value;
Path = tmpPath;
}
tmpPath.pop_back();
}
tmpPath.push_back(x);
for(auto it = pre[x].begin(); it != pre[x].end(); ++it)
dfs(*it);
tmpPath.pop_back();
}
//vector<egde> G[MAXN]; // 邻接表
int a,b,dist;
int main() {
cin >> n >> m >> c1 >> c2;
for(int i = 0; i < n; ++i)cin >> weight[i];
for(int i = 0; i < m; ++i) {
cin >> a >> b >> dist;
G[a].push_back(edge {b,dist});
G[b].push_back(edge {a,dist});
}
// Bellman-Ford 算法
bellman(c1);
dfs(c2);
cout << num << " " << maxValue << endl;
return 0;
}
SPFA
思想:
使用 edge 结构体保存边权,建立邻接表。
struct edge{
int v,dist;
}
- 外层 n-1 轮循环换用 队列 来控制,由此需要多维护两个数组
- inq[] 入队标记
- num[] 入队次数,用于判断负权环
- 内层遍历所有的点,变为 遍历队首 u 的所有边。
步骤:
队不空时循环:
0. 初始化;建队;起点入队
1.出队一个元素,修改 inq 标记
2.所有可走点入队:遍历 u 的所有边,进行松弛;若 v 发生了松弛,判断 inq[v] 并加入队列
代码:
int n,m,c1,c2;
int weight[MAXN]; // 点权
struct edge {
int v;
int dist;
edge(int _v,int _dist):v(_v),dist(_dist) { }
};
vector<edge> G[MAXN]; // 邻接表
int a,b,dist;
int d[MAXN];
set<int> pre[MAXN] ;
bool inq[MAXN]; // 入队标记
int num[MAXN]; // 入队次数
bool SPFA(int s) {
// 0. 初始化
fill(d,d+MAXN,INF);
fill(inq,inq+MAXN,false);
// 初始节点入队
queue<int> qu;
qu.push(s);
inq[s] = true;
num[s]++;
d[s] = 0;
// 队不空时循环
while(!qu.empty()) {
// 出队一个元素
int u = qu.front();
qu.pop();
inq[u] = false; // 修改入队标记
// 所有可走点入队:遍历所有的边,松弛了的点就入队
for(int j = 0; j < G[u].size(); ++j) { // 对应的边
int v = G[u][j].v;
int dist = G[u][j].dist;
// 松弛
if(d[u]+dist < d[v]) {
d[v] = d[u] + dist;
pre[v].clear();
pre[v].insert(u);
// 入队
if(inq[v] == false) {
qu.push(v) ;
inq[v] = true;
// 判断负权环
if(num[v] >= n) return false;
}
} else if(d[u] + dist == d[v]) {
pre[v].insert(u);
}
}
}
return true;
}
vector<int> tmpPath,Path;
int maxValue;
int cnt;
void dfs(int x) {
if(x == c1) {
cnt++;
tmpPath.push_back(x);
int value = 0;
for(int i = 0; i < tmpPath.size(); ++i) value += weight[tmpPath[i]];
if(value > maxValue) {
maxValue = value;
Path = tmpPath;
}
tmpPath.pop_back();
return ;
}
tmpPath.push_back(x);
for(auto it = pre[x].begin(); it != pre[x].end(); ++it) dfs(*it);
tmpPath.pop_back();
}
int main() {
cin >> n >> m >> c1 >> c2;
for(int i = 0 ; i < n; ++i) cin >> weight[i];
for(int i = 0; i < m; ++i) {
cin >> a >> b >> dist;
G[a].push_back(edge {b,dist});
G[b].push_back(edge {a,dist});
}
SPFA(c1);
dfs(c2);
cout << cnt <<" "<< maxValue << endl;
return 0;
}