在上一篇中我们进行了图的专项练习,在这一篇中我们重点探讨图的编程专项习题。
编程题
作为一个城市的应急救援队伍的负责人,你有一张特殊的全国地图。在地图上显示有多个分散的城市和一些连接城市的快速道路。每个城市的救援队数量和每一条连接两个城市的快速道路长度都标在地图上。当其他城市有紧急求助电话给你的时候,你的任务是带领你的救援队尽快赶往事发地,同时,一路上召集尽可能多的救援队。
输入格式:
输入第一行给出4个正整数N、M、S、D,其中N(2≤N≤500)是城市的个数,顺便假设城市的编号为0 ~ (N−1);M是快速道路的条数;S是出发地的城市编号;D是目的地的城市编号。
第二行给出N个正整数,其中第i个数是第i个城市的救援队的数目,数字间以空格分隔。随后的M行中,每行给出一条快速道路的信息,分别是:城市1、城市2、快速道路的长度,中间用空格分开,数字均为整数且不超过500。输入保证救援可行且最优解唯一。
输出格式:
第一行输出最短路径的条数和能够召集的最多的救援队数量。第二行输出从S到D的路径中经过的城市编号。数字间以空格分隔,输出结尾不能有多余空格。
输入样例:
4 5 0 3
20 30 40 10
0 1 1
1 3 2
0 3 3
0 2 2
2 3 2
输出样例:
2 60
0 1 3
#include
#include
#include
using namespace std;
#define inf 0x3f3f3f3f
int map[1010][1010],a[1010],b[1010]; // 定义邻接矩阵map、每个顶点的权值a、到每个顶点的最小权值和b
int dis[1010],vis[1010],l[1010],sum[1010]; // 定义源点到各顶点的距离dis,标记数组vis,前驱数组l,到达某个顶点的最短路径条数sum
int n,m,d,s; // 定义顶点数n、边数m、起点s和终点d
void dijstra(int x) // Dijkstra算法求解最短路径
{
int i,j,k,minn;
b[x]=a[x]; // 初始化起点的最小权值和为a[s]
for(i=0;i<n;i++)
{
dis[i]=map[x][i]; // 初始化源点到各顶点的距离为起点到i的边权值
l[i]=-1; // 初始化前驱数组l为-1,表示没有前驱
if(dis[i]<inf) // 如果存在从起点s到顶点i的边
{
b[i]=a[x]+a[i]; // 更新到顶点i的最小权值和为a[s]+a[i]
sum[i]=1; // 到达顶点i的最短路径条数初始化为1
l[i]=x; // 更新前驱为起点s
}
}
l[x]=-1; // 起点s没有前驱
vis[x]=1; // 标记起点s已经访问过
for(i=0;i<n;i++)
{
minn=inf;
for(j=0;j<n;j++)
{
if(!vis[j]&&dis[j]<minn) // 找到未访问过的距离源点最近的顶点
{
minn=dis[j];
k=j;
}
}
vis[k]=1; // 标记k顶点已经访问过
for(j=0;j<n;j++)
{
if(!vis[j]&&dis[j]>minn+map[k][j]) // 如果通过k顶点可以更新j顶点的最短路径
{
dis[j]=minn+map[k][j]; // 更新源点到j顶点的距离
b[j]=b[k]+a[j]; // 更新到j顶点的最小权值和
l[j]=k; // 更新j顶点的前驱为k顶点
sum[j]=sum[k]; // 更新到达j顶点的最短路径条数
}
else if(!vis[j]&&dis[j]==minn+map[k][j]) // 如果通过k顶点可以到达j顶点的最短路径有多条
{
sum[j]=sum[j]+sum[k]; // 更新到达j顶点的最短路径条数
if(b[j]<b[k]+a[j]) // 如果更新后到达j顶点的最小权值和更小,则更新前驱为k顶点
{
b[j]=b[k]+a[j];
l[j]=k;
}
}
}
}
}
void find(int x) // 递归输出路径上的所有顶点
{
if(l[x]!=-1)
{
find(l[x]);
printf("%d ",l[x]);
}
}
int main()
{
memset(map,inf,sizeof(map));
memset(vis,0,sizeof(vis));
int i,j,x,y,t;
scanf("%d%d%d%d",&n,&m,&s,&d);
for(i=0;i<n;i++)
scanf("%d",&a[i]); // 输入每个顶点的权值
for(i=0;i<m;i++)
{
scanf("%d%d%d",&x,&y,&t);
map[x][y]=map[y][x]=t; // 输入每条边的起点、终点和边权,构造邻接矩阵
}
dijstra(s); // 求解从起点s到各顶点的最短路径
printf("%d %d\n",sum[d],b[d]); // 输出从起点s到终点d的最短路径条数和最小权值和
find(d); // 输出从起点s到终点d的最短路径上的所有顶点
printf("%d\n",d);
return 0;
}
森森喜欢坐地铁。这个假期,他终于来到了传说中的地铁之城——魔都,打算好好过一把坐地铁的瘾!
魔都地铁的计价规则是:起步价 2 元,出发站与到达站的最短距离(即计费距离)每 K 公里增加 1 元车费。
例如取 K = 10,动安寺站离魔都绿桥站为 40 公里,则车费为 2 + 4 = 6 元。
为了获得最大的满足感,森森决定用以下的方式坐地铁:在某一站上车(不妨设为地铁站 A),则对于所有车费相同的到达站,森森只会在计费距离最远的站或线路末端站点出站,然后用森森美图 App 在站点外拍一张认证照,再按同样的方式前往下一个站点。
坐着坐着,森森突然好奇起来:在给定出发站的情况下(在出发时森森也会拍一张照),他的整个旅程中能够留下哪些站点的认证照?
地铁是铁路运输的一种形式,指在地下运行为主的城市轨道交通系统。一般来说,地铁由若干个站点组成,并有多条不同的线路双向行驶,可类比公交车,当两条或更多条线路经过同一个站点时,可进行换乘,更换自己所乘坐的线路。举例来说,魔都 1 号线和 2 号线都经过人民广场站,则乘坐 1 号线到达人民广场时就可以换乘到 2 号线前往 2 号线的各个站点。换乘不需出站(也拍不到认证照),因此森森乘坐地铁时换乘不受限制。
输入格式:
输入第一行是三个正整数 N、M 和 K,表示魔都地铁有 N 个车站 (1 ≤ N ≤ 200),M 条线路 (1 ≤ M ≤ 1500),最短距离每超过 K 公里 (1 ≤ K ≤ 106),加 1 元车费。
接下来 M 行,每行由以下格式组成:
<站点1><空格><距离><空格><站点2><空格><距离><空格><站点3> … <站点X-1><空格><距离><空格><站点X>
其中站点是一个 1 到 N 的编号;两个站点编号之间的距离指两个站在该线路上的距离。两站之间距离是一个不大于 106 的正整数。一条线路上的站点互不相同。
注意:两个站之间可能有多条直接连接的线路,且距离不一定相等。
再接下来有一个正整数 Q (1 ≤ Q ≤ 200),表示森森尝试从 Q 个站点出发。
最后有 Q 行,每行一个正整数 Xi,表示森森尝试从编号为 Xi 的站点出发。
输出格式:
对于森森每个尝试的站点,输出一行若干个整数,表示能够到达的站点编号。站点编号从小到大排序。
输入样例:
6 2 6
1 6 2 4 3 1 4
5 6 2 6 6
4
2
3
4
5
输出样例:
1 2 4 5 6
1 2 3 4 5 6
1 2 4 5 6
1 2 4 5 6
#include
#include
#include
#include
#include
#include
#include
#include
#define int long long
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;
int n, m, k, dp[205][205]; // 定义顶点数n,边数m,每条边的费用k,和表示最短路径的二维数组dp
vector<int> g[205]; // 保存最终关系网
bool vis[205]; // 标记数组,用于DFS访问
vector<int> res; // 最终能经过的点
bool flag[205]; // 记录是否为末端站点
void dfs(int now){ // 深度优先搜索遍历图
for(int i = 0; i < g[now].size(); i++){
int to = g[now][i];
if(!vis[to]){
vis[to] = true;
res.push_back(to);
dfs(to);
}
}
}
signed main()
{
cin >> n >> m >> k; // 输入顶点数、边数和费用
memset(dp, 0x3f, sizeof dp); // 初始化最短路径数组为无穷大
for(int i = 1; i <= m; i++){
int u, v, w;
scanf("%lld", &u);
flag[u] = true;
while(true){
scanf("%lld%lld", &w, &v);
dp[u][v] = dp[v][u] = min(dp[u][v], w); // 更新边的费用
u = v;
char ch = getchar();
if(ch == '\n')
break;
}
flag[u] = true;
}
for(int i = 1; i <= n; i++)
dp[i][i] = 0; // 自身到自身的路径长度为0
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j]); // 动态规划求解最短路径
for(int i = 1; i <= n; i++){
map<int, vector<int>> ans; // 使用map来保存每个价格对应的站点集合
for(int j = 1; j <= n; j++){
if(i == j || dp[i][j] == inf) continue;
int price = dp[i][j]/k; // 计算价格
if(!ans[price].size())
ans[price].push_back(j);
else if(dp[i][ans[price][0]] < dp[i][j]){
ans[price].clear();
ans[price].push_back(j);
}
else if(dp[i][ans[price][0]] == dp[i][j])
ans[price].push_back(j);
}
for(int j = 1; j <= n; j++) // 将末端站点加入
{
if(dp[i][j] != inf && flag[j]){
int price = dp[i][j]/k;
ans[price].push_back(j);
}
}
vector<int> temp;
for(map<int, vector<int>>::iterator it = ans.begin(); it != ans.end(); it++){
for(int j = 0; j < (it->second).size(); j++)
temp.push_back((it->second)[j]);
}
for(int j = 0; j < temp.size(); j++)
g[i].push_back(temp[j]); // 更新最终关系网
}
int q;
cin >> q; // 输入查询次数
while(q--){
int t;
scanf("%lld", &t);
res.clear();
memset(vis, false, sizeof vis);
vis[t] = true;
res.push_back(t);
dfs(t); // 深度优先搜索遍历从起点到终点的路径
sort(res.begin(), res.end()); // 对结果进行排序
printf("%lld", res[0]);
for(int i = 1; i < res.size(); i++)
printf(" %lld", res[i]); // 输出最短路径上经过的点
puts("");
}
return 0;
}
给定一个带权无向图,如果是连通图,则至少存在一棵最小生成树,有时最小生成树并不唯一。本题就要求你计算最小生成树的总权重,并且判断其是否唯一。
输入格式:
首先第一行给出两个整数:无向图中顶点数 N(≤500)和边数 M。随后 M 行,每行给出一条边的两个端点和权重,格式为“顶点1 顶点2 权重”,其中顶点从 1 到N 编号,权重为正整数。题目保证最小生成树的总权重不会超过 230。
输出格式:
如果存在最小生成树,首先在第一行输出其总权重,第二行输出“Yes”,如果此树唯一,否则输出“No”。如果树不存在,则首先在第一行输出“No MST”,第二行输出图的连通集个数。
输入样例 1:
5 7
1 2 6
5 1 1
2 3 4
3 4 3
4 1 7
2 4 2
4 5 5
输出样例 1:
11
Yes
输入样例 2:
4 5
1 2 1
2 3 1
3 4 2
4 1 2
3 1 3
输出样例 2:
4
No
输入样例 3:
5 5
1 2 1
2 3 1
3 4 2
4 1 2
3 1 3
输出样例 3:
No MST
2
#include
#include
#include
using namespace std;
const int N = 200010;
int n,m;
int p[N];
int f = 0;
//并查集
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
//结构体存边
struct Edge{
int a,b,w;
bool operator < (const Edge &x) const{
return w < x.w;
}
} es[N];
int main(){
cin >> n >> m; // 输入顶点数和边数
for(int i = 0;i < m;i ++){
int a,b,c;
cin >> a >> b >> c; // 输入边的起点、终点和权重
es[i] = {a,b,c}; // 存储边的信息到结构体数组
}
int cnt = 0,res = 0; // cnt表示当前生成树边的数量,res表示当前生成树的总权重
for(int i = 1;i <= n;i ++) p[i] = i; // 初始化并查集,每个节点的父节点初始化为自身
sort(es,es + m); // 对边按权重进行排序
for(int i = 0;i < m;i ++){
int a = es[i].a,b = es[i].b,w = es[i].w; // 获取当前边的起点、终点和权重
a = find(a),b = find(b); // 查找起点和终点的根节点
if(a != b){ // 如果起点和终点不在同一个连通块中
for(int j = i + 1;j < m && w == es[j].w;j ++){ // 在相同权重的边中查找是否存在形成环路的边
int pa = find(es[j].a),pb = find(es[j].b);
if((pa == a && pb == b) || (pa == b && pb == a)) f = 1; // 如果存在形成环路的边,将标志位f设为1
}
p[a] = b; // 将起点的根节点设置为终点的根节点,合并两个连通块
res += w; // 更新生成树的总权重
cnt ++; // 边数加1
}
if(cnt == n - 1) break; // 如果生成树边的数量达到n-1,即所有顶点都被连接,则结束循环
}
set<int> alls; // 使用set保存所有顶点所属的连通块的根节点
for(int i = 1;i <= n;i ++) alls.insert(find(i)); // 遍历所有顶点,插入其根节点到set中
if(alls.size() == 1){ // 如果只有一个连通块
cout << res << endl; // 输出生成树的总权重
if(f) cout << "No"; // 如果存在形成环路的边,输出"No"
else cout << "Yes"; // 否则输出"Yes"
}else{
cout << "No MST" << endl; // 输出没有最小生成树
cout << alls.size(); // 输出连通块的数量
}
}
一个旅游景点,如果被带火了的话,就被称为“网红点”。大家来网红点游玩,俗称“打卡”。在各个网红点打卡的快(省)乐(钱)方法称为“攻略”。你的任务就是从一大堆攻略中,找出那个能在每个网红点打卡仅一次、并且路上花费最少的攻略。
输入格式:
首先第一行给出两个正整数:网红点的个数 N(1
再下一行给出一个正整数 K,是待检验的攻略的数量。随后 K 行,每行给出一条待检攻略,格式为:
n V1 V2 ⋯ Vn
其中 n(≤200) 是攻略中的网红点数,Vi 是路径上的网红点编号。这里假设你从家里出发,从 V1 开始打卡,最后从 Vn 回家。
输出格式:
在第一行输出满足要求的攻略的个数。
在第二行中,首先输出那个能在每个网红点打卡仅一次、并且路上花费最少的攻略的序号(从 1 开始),然后输出这个攻略的总路费,其间以一个空格分隔。如果这样的攻略不唯一,则输出序号最小的那个。
题目保证至少存在一个有效攻略,并且总路费不超过 109。
输入样例:
6 13
0 5 2
6 2 2
6 0 1
3 4 2
1 5 2
2 5 1
3 1 1
4 1 2
1 6 1
6 3 2
1 2 1
4 5 3
2 0 2
7
6 5 1 4 3 6 2
6 5 2 1 6 3 4
8 6 2 1 6 3 4 5 2
3 2 1 5
6 6 1 3 4 5 2
7 6 2 1 3 4 5 2
6 5 2 1 4 3 6
输出样例:
3
5 11
样例说明:
第 2、3、4、6 条都不满足攻略的基本要求,即不能做到从家里出发,在每个网红点打卡仅一次,且能回到家里。所以满足条件的攻略有 3 条。
第 1 条攻略的总路费是:(0->5) 2 + (5->1) 2 + (1->4) 2 + (4->3) 2 + (3->6) 2 + (6->2) 2 + (2->0) 2 = 14;
第 5 条攻略的总路费同理可算得:1 + 1 + 1 + 2 + 3 + 1 + 2 = 11,是一条更省钱的攻略;
第 7 条攻略的总路费同理可算得:2 + 1 + 1 + 2 + 2 + 2 + 1 = 11,与第 5 条花费相同,但序号较大,所以不输出。
#include
using namespace std;
int main()
{
int n, m; //网红点的个数 N(1
cin >> n >> m;
int a[201][201]; //邻接矩阵a,存储网红点之间的距离
int x, y, s; //两个网红点xy和路费s
for(int i = 0; i < m; i++)
{
cin >> x >> y >> s;
a[x][y] = s;
a[y][x] = s;
}
int num; //攻略数量
cin >> num;
int cost; // 更新符合条件的攻略的费用
int mincost = 0x3f3f3f3f; //更新符合条件的攻略的最小费用
int sn; //更新符合条件的攻略的序号
int glnum = 0; //更新符合条件的攻略的数量
for(int i = 0; i < num; i++) //第i条攻略
{
int p; //第i条攻略经过p个网红点
cin >> p;
cost = 0;
int cur = 0; //记录当前位置
int arr[201] = {0}; //记录已经到过的网红点
//fill(arr, arr + 201, 0); //全部赋为0,符合!arr[] //不用STL
int flag = 0; //标记该攻略是否符合要求
int k;
for(int j = 0; j < p; j++)
{
cin >> k; //依次读入这p个网红点
if(a[cur][k] && !arr[k]) //如果两网红点可直达且下一网红点没有到达过,否则该路径不符合要求
{
cost += a[cur][k];
arr[k] = 1;
cur = k;
}
else
{
flag = 1;
}
}
cost += a[k][0];
if(p == n && a[k][0] && !flag)
{
if(cost < mincost)
{
mincost = cost;
sn = i + 1;
}
glnum++;
}
}
cout << glnum << endl << sn << ' ' << mincost;
return 0;
}
某地区经过对城镇交通状况的调查,得到现有城镇间快速道路的统计数据,并提出“畅通工程”的目标:使整个地区任何两个城镇间都可以实现快速交通(但不一定有直接的快速道路相连,只要互相间接通过快速路可达即可)。现得到城镇道路统计表,表中列出了有可能建设成快速路的若干条道路的成本,求畅通工程需要的最低成本。
输入格式:
输入的第一行给出城镇数目N (1 输出格式: 输出畅通工程需要的最低成本。如果输入数据不足以保证畅通,则输出“Impossible”。 输入样例1: 输出样例1: 输入样例2: 输出样例2: 给定一幅地图,其中有水域,有陆地。被水域完全环绕的陆地是岛屿。有些岛屿上埋藏有宝藏,这些有宝藏的点也被标记出来了。本题就请你统计一下,给定的地图上一共有多少岛屿,其中有多少是有宝藏的岛屿。 输入格式: 输入第一行给出 2 个正整数 N 和 M(1 输出格式: 在一行中输出 2 个整数,分别是岛屿的总数量和有宝藏的岛屿的数量。 输入样例: 输出样例: 给定长度为 N 的散列表,处理整数最常用的散列映射是 H(x)=x%N。如果我们决定用线性探测解决冲突问题,则给定一个顺序输入的整数序列后,我们可以很容易得到这些整数在散列表中的分布。例如我们将 1、2、3 顺序插入长度为 3 的散列表 但是现在要求解决的是“逆散列问题”,即给定整数在散列表中的分布,问这些整数是按什么顺序插入的? 输入格式: 输入的第一行是正整数 N(≤1000),为散列表的长度。第二行给出了 N 个整数,其间用空格分隔,每个整数在序列中的位置(第一个数位置为0)即是其在散列表中的位置,其中负数表示表中该位置没有元素。题目保证表中的非负整数是各不相同的。 输出格式: 按照插入的顺序输出这些整数,其间用空格分隔,行首尾不能有多余的空格。注意:对应同一种分布结果,插入顺序有可能不唯一。例如按照顺序 3、2、1 插入长度为 3 的散列表,我们会得到跟 1、2、3 顺序插入一样的结果。在此规定:当前的插入有多种选择时,必须选择最小的数字,这样就保证了最终输出结果的唯一性。 输入样例: 输出样例: 假定一个工程项目由一组子任务构成,子任务之间有的可以并行执行,有的必须在完成了其它一些子任务后才能执行。“任务调度”包括一组子任务、以及每个子任务可以执行所依赖的子任务集。 比如完成一个专业的所有课程学习和毕业设计可以看成一个本科生要完成的一项工程,各门课程可以看成是子任务。有些课程可以同时开设,比如英语和C程序设计,它们没有必须先修哪门的约束;有些课程则不可以同时开设,因为它们有先后的依赖关系,比如C程序设计和数据结构两门课,必须先学习前者。 但是需要注意的是,对一组子任务,并不是任意的任务调度都是一个可行的方案。比如方案中存在“子任务A依赖于子任务B,子任务B依赖于子任务C,子任务C又依赖于子任务A”,那么这三个任务哪个都不能先执行,这就是一个不可行的方案。你现在的工作是写程序判定任何一个给定的任务调度是否可行。 输入格式: 输入说明:输入第一行给出子任务数N(≤100),子任务按1~N编号。随后N行,每行给出一个子任务的依赖集合:首先给出依赖集合中的子任务数K,随后给出K个子任务编号,整数之间都用空格分隔。 输出格式: 如果方案可行,则输出1,否则输出0。 输入样例1: 输出样例1: 输入样例2: 输出样例2: 假定一个工程项目由一组子任务构成,子任务之间有的可以并行执行,有的必须在完成了其它一些子任务后才能执行。“任务调度”包括一组子任务、以及每个子任务可以执行所依赖的子任务集。 比如完成一个专业的所有课程学习和毕业设计可以看成一个本科生要完成的一项工程,各门课程可以看成是子任务。有些课程可以同时开设,比如英语和C程序设计,它们没有必须先修哪门的约束;有些课程则不可以同时开设,因为它们有先后的依赖关系,比如C程序设计和数据结构两门课,必须先学习前者。 但是需要注意的是,对一组子任务,并不是任意的任务调度都是一个可行的方案。比如方案中存在“子任务A依赖于子任务B,子任务B依赖于子任务C,子任务C又依赖于子任务A”,那么这三个任务哪个都不能先执行,这就是一个不可行的方案。 任务调度问题中,如果还给出了完成每个子任务需要的时间,则我们可以算出完成整个工程需要的最短时间。在这些子任务中,有些任务即使推迟几天完成,也不会影响全局的工期;但是有些任务必须准时完成,否则整个项目的工期就要因此延误,这种任务就叫“关键活动”。 请编写程序判定一个给定的工程项目的任务调度是否可行;如果该调度方案可行,则计算完成整个工程项目需要的最短时间,并输出所有的关键活动。 输入格式: 输入第1行给出两个正整数N(≤100)和M,其中N是任务交接点(即衔接相互依赖的两个子任务的节点,例如:若任务2要在任务1完成后才开始,则两任务之间必有一个交接点)的数量。交接点按1N编号,M是子任务的数量,依次编号为1M。随后M行,每行给出了3个正整数,分别是该任务开始和完成涉及的交接点编号以及该任务所需的时间,整数间用空格分隔。 输出格式: 如果任务调度不可行,则输出0;否则第1行输出完成整个工程项目需要的时间,第2行开始输出所有关键活动,每个关键活动占一行,按格式“V->W”输出,其中V和W为该任务开始和完成涉及的交接点编号。关键活动输出的顺序规则是:任务开始的交接点编号小者优先,起点编号相同时,与输入时任务的顺序相反。 输入样例: 输出样例: 至此,图的专项练习就完成了。6 15
1 2 5
1 3 3
1 4 7
1 5 4
1 6 2
2 3 4
2 4 6
2 5 2
2 6 6
3 4 6
3 5 1
3 6 1
4 5 10
4 6 8
5 6 3
12
5 4
1 2 1
2 3 2
3 1 3
4 5 4
Impossible
#include
R7-6 寻宝图
1
表示陆地,2
-9
表示宝藏。
注意:两个格子共享一条边时,才是“相邻”的。宝藏都埋在陆地上。默认地图外围全是水域。10 11
01000000151
11000000111
00110000811
00110100010
00000000000
00000111000
00114111000
00110010000
00019000010
00120000001
7 2
#include
R7-7 逆散列问题
HT[]
后,将得到HT[0]=3
,HT[1]=1
,HT[2]=2
的结果。11
33 1 13 12 34 38 27 22 32 -1 21
1 13 12 21 33 34 38 27 22 32
#define _CRT_SECURE_NO_WARNINGS
#include
R7-8 任务调度的合理性
12
0
0
2 1 2
0
1 4
1 5
2 3 6
1 3
2 7 8
1 7
1 10
1 7
1
5
1 4
2 1 4
2 2 5
1 3
0
0
#include
R7-9 关键活动
7 8
1 2 4
1 3 3
2 4 5
3 4 3
4 5 1
4 6 6
5 7 5
6 7 2
17
1->2
2->4
4->6
6->7
#include