A :TT的魔法猫
题目
众所周知,TT 有一只魔法猫。
这一天,TT 正在专心致志地玩《猫和老鼠》游戏,然而比赛还没开始,聪明的魔法猫便告诉了 TT 比赛的最终结果。TT 非常诧异,不仅诧异于他的小猫咪居然会说话,更诧异于这可爱的小不点为何有如此魔力?
魔法猫告诉 TT,它其实拥有一张游戏胜负表,上面有 N 个人以及 M 个胜负关系,每个胜负关系为 A B,表示 A 能胜过 B,且胜负关系具有传递性。即 A 胜过 B,B 胜过 C,则 A 也能胜过 C。
TT 不相信他的小猫咪什么比赛都能预测,因此他想知道有多少对选手的胜负无法预先得知,你能帮帮他吗?
输入
第一行给出数据组数。
每组数据第一行给出 N 和 M(N , M <= 500)。
接下来 M 行,每行给出 A B,表示 A 可以胜过 B
输出
对于每一组数据,判断有多少场比赛的胜负不能预先得知。注意 (a, b) 与 (b, a) 等价,即每一个二元组只被计算一次。
样例
样例输入:
3
3 3
1 2
1 3
2 3
3 2
1 2
2 3
4 2
1 2
3 4
样例输出:
0
0
4
思路
这个就是利用了关系的传递性,如果i->k;k->j则i->j;
若AB 的关系已知,就是dis[A][B]=1
或者是dis[B][A]=1;
使用floyd 算法,将原算法中的dis[i][j]=dis[i][k]+dis[k][j]
改为dis[i][j]=dis[i][k]&dis[k][j]
;
注意的是:0(n3)的时间复杂度过高,因此需要剪枝,因为只有dis[i][k]和dis[k][j]都等于1 的时候结果才为1 ,因此当dis[i][k]的结果是0时,就可以break 。
存储方式是二维矩阵存储。
总结
floyd 算法的基本模板是:
void floyd()
{
for(int k=1;k<n;k++)
for(int i =1;i<=n;i++)
for(int j=1;i<=n;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]); //可以更改
}
复杂度是0(n3),
floyd 算法的应用
- 用于求取图中任意两点之间的关系
- 多源最短路问题,任意两点的距离关系
- 图上的传递闭包,任意两点间的连通关系
代码
#include<stdio.h>
#include<string.h>
using namespace std;
#define maxn 510
int dis[maxn][maxn];
void floyd(int n)
{
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
if(!dis[i][k]) continue;
for(int j=1;j<=n;j++)
{
if(dis[i][j]<dis[i][k]&dis[k][j])
{
dis[i][j]=dis[i][k]&dis[k][j];
}
}
}
}
}
int main()
{
int N;
scanf("%d",&N);
while(N--)
{
memset(dis,0,sizeof(dis));
int n,m;
int a,b;
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&a,&b);
dis[a][b]=1;
}
floyd(n);
int cnt=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
{
if(dis[i][j]!=1&&dis[j][i]!=1)
{
cnt++;
}
}
}
printf("%d\n",cnt);
}
}
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
思路
求取两点间没有负权边的单源最短路问题,用dijkstra算法。比较大的一个问题是,需要有经济线和商业线的组合,但是由于最多只允许出现一条商业线,因此,可以从起点和终点分别使用dijkstra算法,求出到各点的距离,然后枚举商业线,看能否更新。
同时需要注意的是输出问题,因为需要正向输出记录的是前向数组,因此在从起点到终点的递归输出路径中要注意输出的位置,同时需不需要用商业线还需要分开处理。
存储方式:链式前向星
时间复杂度 : 0(n+m)log(n);
总结
dijkstra算法实现大体流程:
-
设置s 为源点,dis[a]表示源点s 到a 的最短距离,初始化dis[s]=0,dis[i]=inf,将s 加入最小堆;
-
每次从堆中取出一个点x,遍历x 的所有邻接边(x,y,w)比较dis[y]与dis[x]+w 的大小。(松弛操作)
-
若松弛成功,更新dis[y] 的大小,将y 加入最小堆中
-
不断执行上述操作,知道最小堆为空,最后得到的dis数组即为所求单源最短路径值。
需要注意的是:
- 由于图中只有正边,因此每个点只会被最小堆弹出一次
- 即一旦某个点被最小堆弹出,则不会被松弛,dis 的值为最短路
(粗浅的理解,由于每个值进去最小堆之前都是加入了一个正数,假如说再被弹出,一定是再被加入,这时候加的正数一定比原来的大,矛盾。因此只会被弹出一次,并且更新后的就是最短值)
模板中可以根据不同的条件改变松弛操作。
代码
#include<stdio.h>
#include<queue>
#include<string.h>
#include<stack>
using namespace std;
#define maxn 1010
#define inf 200000
struct Edge
{
int u,v,w,next;
}edge[2*maxn];
Edge edge1[2*maxn];
int head[maxn];
int head1[maxn];
int tot,tot1;
int dis[maxn];
int dis1[maxn];
int vis[maxn];
int path[maxn];
int path1[maxn];
void init(int n,int *head)
{
tot=1;
for(int i=1;i<=n;i++){
head[i]=-1;
}
}
void addEdge(int u,int v,int w)
{
edge[tot].u=u;
edge[tot].v=v;
edge[tot].w=w;
edge[tot].next=head[u];
head[u]=tot;
tot++;
}
priority_queue<pair<int,int>> q; //大根堆
void dijkstra(int s,int *dis,int n,int *path) //pair 从小到大
{
while(q.size()) q.pop();
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++) {
dis[i]=-inf;
path[i]=-1;
}
dis[s]=0;
path[s]=-1;
q.push(make_pair(0,s));
while(q.size()){
int x=q.top().second;
q.pop();
if(vis[x]) continue;
vis[x]=1;
for(int i=head[x];i;i=edge[i].next)
{
int y=edge[i].v,w=edge[i].w;
if(dis[y]<dis[x]+w){
dis[y]=dis[x]+w;
path[y]=x;
q.push(make_pair(dis[y],y));
}
}
}
}
void outputS(int x,int S)
{
if (x == S)
{
printf("%d", x);
return;
}
outputS(path[x],S);
printf(" %d", x);
}
void outputE(int x,int E)
{
while (x != E)
{
printf(" %d", x);
x = path1[x];
}
printf(" %d", x);
}
void Print(int S,int E) //S-E
{
stack<int> st;
st.push(E);
while(path[E]!=-1){
st.push(path[E]);
E=path[E]; //更新
}
while(!st.empty()) {
if(st.top()!=E)
printf("%d ",st.top()); //多一个空格
else if(st.top()==E)
printf("%d",st.top());
st.pop();
}
}
int main()
{
int N,S,E,M,K,u,v,w,uu=-1,vv=-1;
int flag=0;
while(scanf("%d%d%d",&N,&S,&E)!=EOF)
{
scanf("%d",&M);
init(2*M+1,head);
while(M--)
{
scanf("%d%d%d",&u,&v,&w);
addEdge(u,v,-w); //经济线
addEdge(v,u,-w);
}
dijkstra(S,dis,N,path); //
memset(vis,0,sizeof(vis));
dijkstra(E,dis1,N,path1);
bool bbb=false;
int ans=dis[E];
scanf("%d",&K);
while(K--){
scanf("%d%d%d",&u,&v,&w);
if(ans<dis[u]-w+dis1[v]) {
ans=dis[u]-w+dis1[v];
uu=u;vv=v;
bbb=true;
}
else if(ans<dis[v]-w+dis1[u]){
ans=dis[v]-w+dis1[u];
uu=v;vv=u;
bbb=true;
}
}
//输出路径
if(flag>0) printf("\n");
if (!bbb)
{
outputS(E,S);
printf("\nTicket Not Used\n");
printf("%d\n", -dis[E]);
}
else
{
outputS(uu,S);//S-busa
outputE(vv,E);//busb-E
printf("\n%d\n%d\n", uu, -ans);
}
flag++;
}
return 0;
}
C :TT的美梦
题目
这一晚,TT 做了个美梦!
在梦中,TT 的愿望成真了,他成为了喵星的统领!喵星上有 N 个商业城市,编号 1 ~ N,其中 1 号城市是 TT 所在的城市,即首都。
喵星上共有 M 条有向道路供商业城市相互往来。但是随着喵星商业的日渐繁荣,有些道路变得非常拥挤。正在 TT 为之苦恼之时,他的魔法小猫咪提出了一个解决方案!TT 欣然接受并针对该方案颁布了一项新的政策。
具体政策如下:对每一个商业城市标记一个正整数,表示其繁荣程度,当每一只喵沿道路从一个商业城市走到另一个商业城市时,TT 都会收取它们(目的地繁荣程度 - 出发地繁荣程度)^ 3 的税。
TT 打算测试一下这项政策是否合理,因此他想知道从首都出发,走到其他城市至少要交多少的税,如果总金额小于 3 或者无法到达请悄咪咪地打出 ‘?’。
输入
第一行输入 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 号点的最少税费。
输出
每个询问输出一行,如果不可达或税费小于 3 则输出 ‘?’。
样例
样例输入:
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
样例输出:
Case 1:
3
4
Case 2:
?
?
思路
这个题中的边权相当于两个点差的3次方,因此极有可能出现负环,采用的使用队列的SPFA 算法。SPFA 算法通常用于求解含有负权边的单源最短路径同时可用于判断负环。
经过题意分析就是负环上的边和经过负环的边,总距离小于3 的,和不可达的三种情况输出“?"
SPFA 是选取松弛成功的边加入队列,由于有负权边的出现,一条边可能被弹出多次,但是由于只有n 个点,当弹出次数大于n 次的时候就出现了负环。据此来判断负环。当出现一个负环点的时候将这点做一次dfs ,标记,同时 加入队列的条件改为队列中没有且没有在负环上。
代码
#include<stdio.h>
#include<queue>
#include<string.h>
using namespace std;
# define maxn 100010
# define inf 100000000
# define N 210
struct Edge{
int u,v,w,next;
}E[maxn];
int a[N];
int head[N];
int tot;
int vis[N];
int vis1[N];
int cnt[N];
int dis[N];
void addEdge(int u,int v,int w)
{
E[tot].u=u;
E[tot].v=v;
E[tot].w=w;
E[tot].next=head[u];
head[u]=tot;
tot++;
}
void dfs(int u)
{
vis1[u]=1; //错误点 RE
for(int i=head[u];i;i=E[i].next){
if(!vis1[E[i].v]){
dfs(E[i].v);
}
}
}
void SPFA(int n,int s)
{
queue<int> q;
for(int i=1;i<=n;i++)
{
vis[i]=0;
cnt[i]=0;
vis1[i]=0;
dis[i]=100000000; //数据范围不能超过int WA
}
dis[s] = 0;
vis[s]=1;
q.push(s);
while(q.size()){
int u =q.front(); q.pop();
//每一个点可以被队列弹出很多次
vis[u] = 0;
//松弛操作
for(int i=head[u];i;i=E[i].next){
int v = E[i].v;
if(dis[v] >dis[u]+E[i].w)
{
dis[v] = dis[u] + E[i].w;
cnt[v]++;
if(cnt[v]>n){
//出现了负环
dfs(v);
continue ;
}
if(!vis[v]&&vis1[v]==0) {
q.push(v);
vis[v]=1;
}
}
}
}
}
int main()
{
int T,n,M,A,B,Q,P;
scanf("%d",&T);
for(int t=1;t<=T;t++)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
scanf("%d",&M);
tot=1;
memset(head,-1,sizeof(head));
while(M--)
{
scanf("%d%d",&A,&B);
addEdge(A,B,(a[B]-a[A])*(a[B]-a[A])*(a[B]-a[A])); //构造边
}
// 算出来数组 res 从1号点到其他点的最小税费
SPFA(n,1);
scanf("%d",&Q);
printf("Case %d:\n",t);
for(int k=1;k<=Q;k++)
{
scanf("%d",&P);
if(dis[P]<3||vis1[P]||dis[P]==inf) {
printf("?\n");
}
else
printf("%d\n",dis[P]);
}
}
return 0;
}