A - TT 的魔法猫
题目
众所周知,TT 有一只魔法猫。
这一天,TT 正在专心致志地玩《猫和老鼠》游戏,然而比赛还没开始,聪明的魔法猫便告诉了 TT 比赛的最终结果。TT 非常诧异,不仅诧异于他的小猫咪居然会说话,更诧异于这可爱的小不点为何有如此魔力?
魔法猫告诉 TT,它其实拥有一张游戏胜负表,上面有 N 个人以及 M 个胜负关系,每个胜负关系为 A B,表示 A 能胜过 B,且胜负关系具有传递性。即 A 胜过 B,B 胜过 C,则 A 也能胜过 C。
TT 不相信他的小猫咪什么比赛都能预测,因此他想知道有多少对选手的胜负无法预先得知,你能帮帮他吗?
Input
第一行给出数据组数。
每组数据第一行给出 N 和 M(N , M <= 500)。
接下来 M 行,每行给出 A B,表示 A 可以胜过 B。
Output
对于每一组数据,判断有多少场比赛的胜负不能预先得知。注意 (a, b) 与 (b, a) 等价,即每一个二元组只被计算一次。
Sample Input
3
3 3
1 2
1 3
2 3
3 2
1 2
2 3
4 2
1 2
3 4
Sample Output
0
0
4
解题思路
胜负关系具有传递性,因此可以用 Floyd 算法求出
任意两点的胜负关系(传递闭包),即可求出答案。
• dis[a][b] = 1 表示 a 比 b 强
• dis[a][b] = 0 表示 a 与 b 的胜负关系不明
• dis[a][b] = 0 且 dis[b][a] = 0 即表示 a 与 b 的胜负关系无法预先判断
Floyd算法
这个题目里要改变一下形式,不再是存距离最小值,而是保留传递关系,1表示传递,0表示没有传递关系,所以取最大值,有1则保留传递性。
dis[ii][jj] = max(dis[ii][jj], dis[ii][kk] && dis[kk][jj]);
剪枝:当 dis[i][k] = 0 时,i - k - j 一定不具有传递性,所以由原有 dis[i][j] 决定,不用进行下面的判断。
if(dis[ii][kk] == 0) continue;
完整代码
#include <iostream>
#include <string.h>
#include <algorithm>
#include <cmath>
using namespace std;
int k, n, m;
int a, b;
bool dis[510][510];
void floyd()
{
for(int kk=1;kk<=n;kk++)
for(int ii=1;ii<=n;ii++)
{
if(dis[ii][kk] == 0) continue;
for(int jj=1;jj<=n;jj++)
dis[ii][jj] = max(dis[ii][jj], dis[ii][kk] && dis[kk][jj]);
}
}
int main()
{
cin>>k;
for(int i=0;i<k;i++)
{
cin>>n>>m;
memset(dis, 0, sizeof(dis));
for(int j=0;j<m;j++)
{
cin>>a>>b;
dis[a][b] = 1;
}
floyd();
int cnt = 0;
for(int ii=1;ii<=n;ii++)
{
for(int jj=ii+1;jj<=n;jj++)
{
if(dis[ii][jj]==0 && dis[jj][ii]==0)
cnt++;
}
}
cout<<cnt<<endl;
}
return 0;
}
B - TT 的旅行日记
题目
众所周知,TT 有一只魔法猫。
今天他在 B 站上开启了一次旅行直播,记录他与魔法猫在喵星旅游时的奇遇。 TT 从家里出发,准备乘坐猫猫快线前往喵星机场。猫猫快线分为经济线和商业线两种,它们的速度与价钱都不同。当然啦,商业线要比经济线贵,TT 平常只能坐经济线,但是今天 TT 的魔法猫变出了一张商业线车票,可以坐一站商业线。假设 TT 换乘的时间忽略不计,请你帮 TT 找到一条去喵星机场最快的线路,不然就要误机了!
输入
输入
包含多组数据。每组数据第一行为 3 个整数 N, S 和 E (2 ≤ N ≤ 500, 1 ≤ S, E ≤ 100),即猫猫快线中的车站总数,起点和终点(即喵星机场所在站)编号。
下一行包含一个整数 M (1 ≤ M ≤ 1000),即经济线的路段条数。
接下来有 M 行,每行 3 个整数 X, Y, Z (1 ≤ X, Y ≤ N, 1 ≤ Z ≤ 100),表示 TT 可以乘坐经济线在车站 X 和车站 Y 之间往返,其中单程需要 Z 分钟。
下一行为商业线的路段条数 K (1 ≤ K ≤ 1000)。
接下来 K 行是商业线路段的描述,格式同经济线。
所有路段都是双向的,但有可能必须使用商业车票才能到达机场。保证最优解唯一。
输出
对于每组数据,输出3行。第一行按访问顺序给出 TT 经过的各个车站(包括起点和终点),第二行是 TT 换乘商业线的车站编号(如果没有使用商业线车票,输出"Ticket Not Used",不含引号),第三行是 TT 前往喵星机场花费的总时间。
本题不忽略多余的空格和制表符,且每一组答案间要输出一个换行
输入样例
4 1 4
4
1 2 2
1 3 3
2 4 4
3 4 5
1
2 4 3
输出样例
1 2 4
2
5
题解思路
这是一个求最短路的题目,但是增加了商业线的约束。要求输出符合要求的最短路长度,记录的最短路路径,以及记录的换乘点。
• 跑一次单源最短路(变形),记录答案 dis[u][0/1]
• dis[u][0] 表示从起点到结点 u 没有经过商业线时的最短
路,在松弛的时候可以选择商业线或者经济线
• dis[u][1] 表示从起点到结点 u 经过商业线后的最短路,
在松弛的时候只能选择经济线
松弛操作:当遍历的边是经济线时,分别对 dis[u][0/1] 进行松弛操作;当该边是商业线时,用未使用经济线的 dis[u][0] 对 dis[u][1] 进行松弛操作。
if(dis[y][0]>dis[x][0]+ww) //没有用过商业线的 + 经济线 = 经济线
if(!t) //经济线
{
if(dis[y][0]>dis[x][0]+ww) //没有用过商业线的 + 经济线 = 经济线
{
//
}
if(dis[y][1]>dis[x][1]+ww) //用过商业线的 + 经济线 = 商业线
{
//
}
}
if(t) //商业线
{
if(dis[y][1]>dis[x][0]+ww) //只能是没有用过商业线的
{
//
}
}
路径输出:在松弛操作时顺便记录前驱节点pre,然后用递归倒推用数组记录路径,然后输出。
注意:严格按照输出格式,不能有任何多余的空格空行,两组数据之间有空行,末尾没有。
完整代码
#include <iostream>
#include <vector>
#include <queue>
#include <cstdio>
#include <cmath>
#include <string.h>
using namespace std;
int n, S, E;
int m, k, cnt = 0, off = -1;
int dis[510][2];
int road[510];
struct pre_p
{
int xx, tp;
};
pre_p pre[510][2];
struct edge
{
bool type; //0是经济线 1是商业线
int to, w;
bool operator<(const edge &ee)const
{
return w > ee.w;
}
};
vector<edge> e[510];
void make_graph()
{
scanf("%d", &m);
for(int i=0;i<m;i++)
{
int f, t, ww;
scanf("%d %d %d", &f, &t, &ww);
e[f].push_back({0, t, ww});
e[t].push_back({0, f, ww});
}
scanf("%d", &k);
for(int i=0;i<k;i++)
{
int f, t, ww;
scanf("%d %d %d", &f, &t, &ww);
e[f].push_back({1, t, ww});
e[t].push_back({1, f, ww});
}
}
void dijkstra(int s)
{
priority_queue<edge> q;
memset(pre, 0, sizeof(pre));
for(int i=0;i<510;i++)
{
dis[i][0] = 100000000;
dis[i][1] = 100000000;
}
dis[s][0] = 0; dis[s][1] = 0;
q.push({0, s, 0});
while(!q.empty())
{
int x = q.top().to;
q.pop();
for(int i=0;i<e[x].size();i++)
{
int y = e[x][i].to, ww = e[x][i].w, t = e[x][i].type;
if(!t) //经济线
{
if(dis[y][0]>dis[x][0]+ww) //没有用过商业线的 + 经济线 = 经济线
{
pre_p pp;
pp.xx = x;
pp.tp = t;
pre[y][0] = pp;
dis[y][0] = dis[x][0]+ww;
q.push({0, y, dis[y][0]});
}
if(dis[y][1]>dis[x][1]+ww) //用过商业线的 + 经济线 = 商业线
{
pre_p pp;
pp.xx = x;
pp.tp = t;
pre[y][1] = pp;
dis[y][1] = dis[x][1]+ww;
q.push({1, y, dis[y][1]});
}
}
if(t) //商业线
{
if(dis[y][1]>dis[x][0]+ww) //只能是没有用过商业线的
{
pre_p pp;
pp.xx = x;
pp.tp = t;
pre[y][1] = pp;
dis[y][1] = dis[x][0]+ww;
q.push({1, y, dis[y][1]});
}
}
}
}
}
void find(int end, int p)
{
if(end == S)
{
road[cnt] = S;
cnt++;
return;
}
pre_p before = pre[end][p];
if(before.tp == 1)
{
off = before.xx;
p = 0;
}
find(before.xx, p);
road[cnt] = end;
cnt++;
return;
}
int main()
{
int kk = 0;
while(scanf("%d", &n) != EOF)
{
scanf("%d %d", &S, &E);
make_graph();
dijkstra(S);
cnt = 0; off = -1;
int ans;
memset(road, 0, sizeof(road));
if(dis[E][0] < dis[E][1])
{
ans = dis[E][0];
find(E, 0);
}
else
{
ans = dis[E][1];
find(E, 1);
}
if(!kk) kk = 1;
else printf("\n");
for(int i=0;i<cnt;i++)
{
if(i != cnt-1) printf("%d ", road[i]);
if(i == cnt-1) printf("%d\n",road[i]);
}
if(off == -1) printf("Ticket Not Used\n");
else printf("%d\n", off);
printf("%d\n", ans);
for(int i=0;i<510;i++) e[i].clear();
}
return 0;
}
C - TT 的美梦
题目
这一晚,TT 做了个美梦!
在梦中,TT 的愿望成真了,他成为了喵星的统领!喵星上有 N 个商业城市,编号 1 ~ N,其中 1 号城市是 TT 所在的城市,即首都。
喵星上共有 M 条有向道路供商业城市相互往来。但是随着喵星商业的日渐繁荣,有些道路变得非常拥挤。正在 TT 为之苦恼之时,他的魔法小猫咪提出了一个解决方案!TT 欣然接受并针对该方案颁布了一项新的政策。
具体政策如下:对每一个商业城市标记一个正整数,表示其繁荣程度,当每一只喵沿道路从一个商业城市走到另一个商业城市时,TT 都会收取它们(目的地繁荣程度 - 出发地繁荣程度)^ 3 的税。
TT 打算测试一下这项政策是否合理,因此他想知道从首都出发,走到其他城市至少要交多少的税,如果总金额小于 3 或者无法到达请悄咪咪地打出 ‘?’。
Input
第一行输入 T,表明共有 T 组数据。(1 <= T <= 50)
对于每一组数据,第一行输入 N,表示点的个数。(1 <= N <= 200)
第二行输入 N 个整数,表示 1 ~ N 点的权值 a[i]。(0 <= a[i] <= 20)
第三行输入 M,表示有向道路的条数。(0 <= M <= 100000)
接下来 M 行,每行有两个整数 A B,表示存在一条 A 到 B 的有向道路。
接下来给出一个整数 Q,表示询问个数。(0 <= Q <= 100000)
每一次询问给出一个 P,表示求 1 号点到 P 号点的最少税费。
Output
每个询问输出一行,如果不可达或税费小于 3 则输出 ‘?’。
Sample Input
2
5
6 7 8 9 10
6
1 2
2 3
3 4
1 5
5 4
4 5
2
4
5
10
1 2 4 4 5 6 7 8 9 10
10
1 2
2 3
3 1
1 4
4 5
5 6
6 7
7 8
8 9
9 10
2
3 10
Sample Output
Case 1:
3
4
Case 2:
?
?
题解思路
含有负权环路的单源最短路问题,Floyd多源算法时间复杂度高,dijkstra无法处理负权的情况,所以使用SPFA。
SPFA
• 第一轮,与 S 邻接的点被松弛 -> 最短路径上的第一条边
• 第二轮,与第一轮被松弛的点相邻接的点被松弛 -> 最短路径上的第二条边
这样我们不妨每次只做有效的松弛操作
• 建立一个队列
• 队列中存储被成功松弛的点
• 每次从队首取点并松弛其邻接点
• 如果邻接点成功松弛则将其放入队列
负权环路:判断点的入队次数,如果某一点入队 n 次则说明有负环,然后以该点为起点遍历,找到所有联通的点打上标记。
完整代码
#include <iostream>
#include <cmath>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;
struct edge
{
int from, to, w;
};
int T, N, M, Q;
int a[210];
int dis[210];
int cnt[210];
int inq[210];
int vis[210];
vector<edge> e[210];
void make_graph(int m)
{
for(int i=0;i<m;i++)
{
int f, t, ww;
cin>>f>>t;
ww = pow(a[t] - a[f], 3);
e[f].push_back({f, t, ww}); //有向图
}
}
void tag_ring(int ss)
{
queue<int> q;
q.push(ss);
vis[ss] = 1;
while(!q.empty())
{
int x = q.front();
q.pop();
for (int i=0;i<e[x].size();i++)
{
int y = e[x][i].to;
if(!vis[y])
{
q.push(y);
vis[y] = 1;
}
}
}
}
void SPFA(int s)
{
for(int i=1;i<=N;i++)
{
dis[i] = 10000000;
cnt[i] = 0;
inq[i] = 0;
vis[i] = 0;
}
queue<int> q;
dis[s] = 0;
inq[s] = 1;
q.push(s);
while(!q.empty())
{
int u = q.front();
q.pop();
inq[u] = 0; //现在不在队列里了
for(int i=0;i<e[u].size();i++)
{
int v = e[u][i].to;
if(dis[v] > dis[u] + e[u][i].w)
{
cnt[v] = cnt[u] + 1;
if(cnt[v] >= N)
{
tag_ring(v);
}
dis[v] = dis[u] + e[u][i].w;
if(!inq[v] && !vis[v])
{
q.push(v);
inq[v] = 1;
}
}
}
}
}
int main()
{
cin>>T;
int k = 1;
while(T--)
{
memset(a, 0, sizeof(a));
cin>>N;
for(int i=1;i<=N;i++) cin>>a[i];
cin>>M;
make_graph(M);
cin>>Q;
SPFA(1);
cout<<"Case "<<k<<':'<<endl;
k++;
while(Q--)
{
int p;
cin>>p;
if(vis[p] || dis[p] < 3 || dis[p] == 10000000) cout<<'?'<<endl;
else cout<<dis[p]<<endl;
}
for(int i=0;i<210;i++) e[i].clear();
}
return 0;
}