emmmm居然放题第一天就AK了。。。。
不过主要是有好多题前段时间做过emmmm
要是什么时候做further也能有这个效率就好了emmmm
我好菜啊
A - The Road to Radio
今天稀的泥心情很好,没有带背包就出门了。然而他出门之后发现,由于自己之前铺的路太多了,有的路可能要绕很远才能到热的油的家。
已知稀的泥和热的油分别居住在的小区,以及稀的泥用稀的泥铺的所有道路,请你计算出稀的泥沿自己铺的路至少要走多远才能到热的油的小区。
Input
题目包含多组数据。
每组数据第一行包含两个正整数N和M(0
Output
对于每组数据,请在一行里输出稀的泥最短需要走的距离。如果不存在从S到T的路线,就输出-1.
Sample Input
3 3
0 1 1
0 2 3
1 2 1
0 2
3 1
0 1 1
1 2
Sample Output
2
-1
Hint
不要问干的土在哪里。干的土在二基楼迷路了。
题意很简单,在一个图中找S到T的最短距离
显然一个单源最短路径问题。单源最短路径指的是固定一个起点,求它到其他所有点的最短路的问题。对于这类问题,我们可以使用Dijkstra或者Bellman-Ford来解决。
Bellman-Ford和Dijkjstra
简单说一下这两种算法:
Bellman-Ford是利用边来放缩,而Dijkstra是利用点来放缩。
Bellman-Ford是一种DP,而Dijkstra是一种贪心。
Bellman-Ford可以处理负权边,而Dijkstra不能。
可以利用Bellman-Ford来判断图中是否存在负圈。
在利用好的数据结构的前提下,Bellman-Ford的时间复杂度为O(|V||E|),而Dijkstra的时间复杂度为O(|E|log|V|)。
Bellman-Ford的空间复杂度为O(|E|),而Dijkstra的空间复杂度为O(|V|²),可以通过题目给的数据范围判断具体用哪种。
Bellman-ford
Bellman-Ford作为一种DP算法,其状态转移方程为:
d[i] = min{d[j]+e[i][j].cost}
其中e[i][j].cost代表的是从i到j的边的距离,若这条边不存在则取值为inf,d[i]表示从起点s出发到顶点i的最短距离。
不难发现,这个状态转移方程的意思,就是说到达点i,可以先到达点j,再利用i到j所连接的边到达i,如果这样会使路径缩短的话,那么就更新它。
从状态转移方程可以发现,d[i]和满足1<=j<=n的所有d[j]都有关,也就是说一旦在更新d数组的过程中改变了某一个元素(即发现到某点的更短的路径),那么这个d数组可能就不是最终结果的d数组,因为还有可能通过这个更新的点使得到其他点的最短路径变短。
那么我们的更新要到什么时候才停止呢?如果图中不存在从s可达的负圈,那么最短路不会经过同一个顶点两次。因为每次遍历所有的边,一定能让至少一个点的最短路径被确定下来(尽管我们无法知道是哪个点)。所以对于图中的n个点,d[s]=0是已知的,那么就至多进行|V|-1次循环,即可得到所有点的最短路。
同时利用这个性质,我们也可以判断图中是否存在负圈。因为如果图中存在负圈的话,循环可以一直进行下去,因为我不断的绕负圈走就可以无限的缩短到其他点的距离。也就是说我们可以尝试进行第|V|次循环,若在这次循环中仍然有d[i]被更新的话,就说明图中存在负圈。
最后贴一个从挑战上抄的板子:
//从顶点from指向顶点to的权值为cost的边
struct edge { int from, to, cost; };
edge es[MAX_E]; //边
int d[MAX_V]; //最短距离
int V, E; //V是顶点数,E是边数
//求解从顶点s出发到所有点的最短距离
void shortest_path(int s) {
for(int i = 0; i < V; i++) d[i] = INF;
d[s] = 0;
while(true) {
bool update = false;
for(int i = 0; i < E; i++) {
edge e = es[i];
if(d[e.from] != INF && d[e.to] > d[e.from] + e.cost) {
d[e.to] = d[e.from] + e.cost;
update = true;
}
}
if(!update) break;
}
}
挑战上判断负圈的板子
//如果返回true则存在负圈
bool find_negative_loop() {
memset(d, 0, sizeof(d));//上面的板子用的INF,但这里是0,可以视为不限定从起点s出发,而是可以从任意一个点出发
for(int i = 0; i < V; i++) {
for(int j = 0; j < E; j++) {
edge e = es[j];
if(d[e.to] > d[e.from] + e.cost) {
d[e.to] = d[e.from] + e.cost;
//如果第n次仍然更新了,则存在负圈
if(i == V-1) return true;
}
}
}
}
从第一个板子改成的判断负圈的板子,效率不知道哪个高(肯定差不多),但是emmmm毕竟可以少记一个板子了。
//从顶点from指向顶点to的权值为cost的边
struct edge { int from, to, cost; };
edge es[MAX_E]; //边
int d[MAX_V]; //最短距离
int V, E; //V是顶点数,E是边数
//求解从顶点s出发到所有点的最短距离
//如果返回true则存在负圈
bool shortest_path(int s) {
for(int i = 0; i < V; i++) d[i] = INF;
d[s] = 0;
for(int i = 1; i < V; i++) {//限制只能修改V-1次
bool update = false;
for(int j = 0; j < E; j++) {
edge e = es[j];
if(d[e.from] != INF && d[e.to] > d[e.from] + e.cost) {
d[e.to] = d[e.from] + e.cost;
update = true;
}
}
if(!update) break;
}
//尝试进行第V次更新
for(int i = 0; i < E; i++) {
edge e = es[i];
if(d[e.from] != INF && d[e.to] > d[e.from] + e.cost)
return true;//第V次仍然产生了更新,说明存在负圈
}
return false;
}
Dijkstra
Dijkstra是一种贪心算法,它的贪心策略是:
1.找到最短距离已经确定的点,从它出发更新相邻顶点的最短距离
2.此后不需要再关心1中的“最短距离已经确定的顶点”
这个贪心策略为什么成立呢?因为从当前距离最短的点出发,到其他图中所有点的距离都会比这个点大,也就是说无法通过其他点放缩来缩小到这个点的距离。所以此时到这个点的距离的临时值也就是最终到这个点的距离,因此可以利用这个点来对其他点进行放缩。
注意一句话:到其他图中所有点的距离都会比这个点大。如果这个条件不成立的话,这个贪心策略也就不成立了。所以我们要求Dijkstra不能处理负权边。
还是抄挑战上的Dijkstra板子:
int cost[MAX_V][MAX_V]; //cost[u][v]表示边e=(u,v)的权值(不存在这条边时设为INF)
int d[MAX_V]; //顶点s出发的最短距离
bool used[MAX_V]; //已经使用过的图
int V; //顶点数
//求从起点s出发到各个顶点的最短距离
void dijkstra(int s) {
fill(d, d+V, INF);
fill(used, used + V; false);
d[s] = 0;
while(true) {
int v = -1;
//从尚未使用过的顶点中选择一个距离最小的顶点
for(int u = 0; u < V; u++) {//这里可以用堆优化到O(log|V|)
if(!used[u) && (v == -1 || d[u] < d[v])) v = u;
}
if(v == -1) break;
used[v] = true;
for(int u = 0; u < V; u++) {//这里可以用邻接表优化到O(|E|)
d[u] = min(d[u], d[v] + cost[u][v]);
}
}
}
挑战上的一个优化板子
struct edge { int to, cost; };
typedef pair<int, int> P;//first是最短距离,second是顶点的编号
int V;
vctor<edge> G[MAX_V];
int d[MAX_V];
void dijkstra(int s) {
//通过指定greater<P>参数,堆按照first从小到大的顺序取出值
priority_queue<P, vector<P>, greater<P> > que;
fill(d, d+V, INF);
d[s] = 0;
que.push(P(0, s));
while(!que.empty()) {
P p = que.top(); que.pop();
int v = p.second;
if(d[v] < p.first) continue;
for(int i = 0; i < G[v].size; i++) {
edge e = G[v][i];
if(d[e.to] > d[v] + e.cost) {
d[e.to] = d[v] + e.cost;
que.push(P(d[e.to], e.to));
}
}
}
}
哇啰嗦了这么多,跑题好远啊。其实这题就是单纯的一个板子题,也就不给太多解释了。直接上代码emmmmmm。
Dijkstra版本
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 205
#define maxm 1005
#define inf 0x3f3f3f3f
using namespace std;
int n, m, s, t;
int dis[maxn][maxn], vis[maxn], d[maxn];
void dijkstra(int s) {
memset(d, inf, sizeof(d));
memset(vis, 0, sizeof(vis));
d[s] = 0;
while(1) {
int small = -1;
for(int i = 0; i < n; i++)
if(!vis[i] && (small == -1 || d[i] < d[small])) small = i;
if(small == -1) break;
vis[small] = 1;
for(int i = 0; i < n; i++)
d[i] = min(d[i], d[small] + dis[small][i]);
}
}
int main() {
while(cin>>n>>m) {
memset(dis, inf, sizeof(dis));
for(int i = 0; i < m; i++) {
int from, to, cost;
cin>>from>>to>>cost;
dis[from][to] = dis[to][from] = min(dis[from][to], cost);
}
for(int i = 0; i < n; i++) dis[i][i] = 0;
cin>>s>>t;
dijkstra(s);
if(d[t] == inf) cout<<-1<<endl;
else cout<<d[t]<<endl;
}
return 0;
}
Bellman-Ford版本
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 205
#define maxm 1005
#define inf 0x3f3f3f3f
using namespace std;
struct Node {
int from, to, cost;
} e[maxm*2];
int n, m, s, t;
int d[maxn];
void bellman_ford(int s) {
memset(d, inf, sizeof(d));
d[s] = 0;
while(true) {
bool update = false;
for(int i = 0; i < m+m; i++) {
if(d[e[i].from] != inf && d[e[i].to] > d[e[i].from] + e[i].cost) {
d[e[i].to] = d[e[i].from] + e[i].cost;
update = true;
}
}
if(!update) break;
}
}
int main() {
while(cin>>n>>m) {
for(int i = 0; i < m; i++) {
int from, to, cost;
cin>>from>>to>>cost;
e[i+i].from = e[i+i+1].to = from;
e[i+i].to = e[i+i+1].from = to;
e[i+i].cost = e[i+i+1].cost = cost;
}
cin>>s>>t;
bellman_ford(s);
if(d[t] == inf) cout<<-1<<endl;
else cout<<d[t]<<endl;
}
return 0;
}
B - 耳机楼的通道
耳机楼里有很多教室,这些教室由双向走廊连接。另外,还存在一些单向的秘密通道,通过它们可以回到过去。现在有 N (1 ≤ N ≤ 500) 个教室,编号 1..N, M (1 ≤ M ≤ 2500) 条走廊,和 W (1 ≤ W ≤ 200) 条秘密通道。
DY在养猫之余,还是一个时间旅行爱好者。她希望从一间教室出发,经过一些走廊和秘密通道,回到她出发之前的某个时间。
共有F (1 ≤ F ≤ 5) 组数据。对每组数据,判断DY是否有回到过去的可能性。不存在耗时超过10,000秒的走廊,且不存在能带DY回到10,000秒之前的秘密通道。
Input
首先是一个整数F,表示接下来会有F组数据。
每组数据第1行:分别是三个空格隔开的整数:N,M和W
第2行到M+1行:三个空格分开的数字(S,E,T)描述双向走廊:从S到E需要耗费T秒。两个教室可能由一个以上的路径来连接。
第M +2到M+ W+1行:三个空格分开的数字(S,E,T)描述秘密通道:从S到E可以使时间倒流T秒。
Output
F行,每行对应一组数据。 每组数据输出单独的一行,” YES”表示能满足要求,”NO”表示不能满足要求。
Sample Input
2
3 3 1
1 2 2
1 3 4
2 3 1
3 1 3
3 2 1
1 2 3
2 3 4
3 1 8
Sample Output
NO
YES
题意:判断图中是否存在负圈,如果有则输出YES,否则输出NO
判断负圈常用的方法有两种:Bellman-Ford和Floyd-Warshall,前者在上一题介绍了,所以这里只介绍Floyd-Warshall
Floyd-Warshall
Floyd-Warshall是处理图中任意两点的最短距离的算法,比起单源最短路问题问题高了一个维度。
Floyd-Warshall和Bellman-Ford一样,也是一个DP算法,其状态转移方程为:
d[i][j] = min(d[i][j], d[i][k] + d[k][j])
这两种算法的DP策略思路是类似的,只不过Bellman-Ford由于起点是固定的,故DP的维度下降了一维。这两种算法都是考虑:要到达i,可以先到j,再利用i到j的边到达i。
这两种算法都可以处理负权边,同时也能判断图中是否存在负圈。Floyd-Warshall判断负圈的方法比较简单,因为他处理了整个图,所以只要判断是否存在有d[i][i] < 0的i,即可说明是否存在负圈。
Floyd的-Warshall算法复杂度为O(|V|³),代码并不长,只是一个三重循环。但时间上的复杂度对这个算法的使用造成了一些限制。
还是挑战上的代码。
int d[MAX_V][MAX_V]; //d[u][v]表示边e=(u,v)的权值(不存在时设为INF,不过d[i][i]=0)
int V;
void warshall_floyd() {
for(int k = 0; k < V; k++)
for(int i = 0; i < V; i++)
for(int j = 0; j < V; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
还是板子题,所以也不说太多。这题同样用两种算法都能做,但是Floyd因为复杂度太高,所以有很多限制。比如如果用min函数就会T,如果用cin也会T。
Floyd版本:
#include<cstdio>
#include<cstring>
#define inf 0x3f3f3f3f
#define maxn 505
#define maxm 2705
int n,m,w;
int dis[maxn][maxn], d[maxn];
int floyd() {
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++) {
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];
if(dis[i][i] < 0) return 1;
}
return 0;
}
int main() {
int T; scanf("%d",&T);
while(T--) {
memset(dis, inf, sizeof(dis));
scanf("%d%d%d",&n,&m,&w);
for(int i = 1; i <= n; i++) dis[i][i] = 0;
while(m--) {
int from, to, cost; scanf("%d%d%d",&from, &to, &cost);
if(dis[from][to] > cost)
dis[from][to] = dis[to][from] = cost;
}
while(w--) {
int from, to, cost; scanf("%d%d%d",&from, &to, &cost);
if(dis[from][to] > -cost)
dis[from][to] = -cost;
}
if(floyd()) printf("YES\n");
else printf("NO\n");
}
return 0;
}
bellman-ford版本:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define inf 0x3f3f3f3f
#define maxn 505
#define maxm 5205
using namespace std;
struct Node {
int from, to, cost;
} e[maxm];
int n,m,w;
int d[maxn];
bool findNeg() {
memset(d, 0, sizeof(d));
for(int i = 1; i < n; i++) {
bool update = false;
for(int j = 1; j <= m+m+w; j++)
if(d[e[j].from] != inf && d[e[j].to] > d[e[j].from] + e[j].cost) {
d[e[j].to] = d[e[j].from] + e[j].cost;
update = true;
}
if(!update) break;
}
for(int j = 1; j <= m+m+w; j++)
if(d[e[j].from] != inf && d[e[j].to] > d[e[j].from] + e[j].cost)
return true;
return false;
}
int main() {
int T; cin>>T;
while(T--) {
cin>>n>>m>>w;
for(int i = 1; i <= m; i++) {
int from, to, cost; cin>>from>>to>>cost;
e[i+i-1].from = e[i+i].to = from;
e[i+i-1].to = e[i+i].from = to;
e[i+i-1].cost = e[i+i].cost = cost;
}
for(int i = 1; i <= w; i++) {
int from, to, cost; cin>>from>>to>>cost;
e[i+m+m].from = from; e[i+m+m].to = to; e[i+m+m].cost = -cost;
}
if(findNeg()) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
return 0;
}
C - 设计网络
存在许多点和点与点之间的路径,路径长度不一,点到点之间可能存在多条路径。挑选部分路径使得所有点连通且总路径长度最小。
Input
多样例输入。每个样例都保证有解。
每个样例的第一行有两个整数,P(点的个数),R(点与点之间的路径总数)。接下来的R行输入路径,每行含3个整数,前两个数表示连接的两个点,第三个数表示路径长度。
当P为0时输入结束。样例与样例之间存在空行
P最大为50,路径长度最大为100,路径数没有限制。
i和j之间的路径可以表示为 i j 或 j i
Output
对于每个样例,输出一个数表示设计出的路径总长度
Sample Input
1 0
2 3
1 2 37
2 1 17
1 2 68
3 7
1 2 19
2 3 11
3 1 7
1 3 5
2 3 89
3 1 91
1 2 32
5 7
1 2 5
2 3 7
2 4 8
4 5 11
3 5 10
1 5 6
4 2 12
0
Sample Output
0
17
16
26
这是求MST的板子题,MST是指图中的最小生成树,即图中所有边权值之和最小的连通子图。
解决MST问题通常有两种方法(怎么又是两种,哈哈哈):Prim算法和Kruskal算法。
Prim和Kruskal
简单说一下这两种算法:
这两种算法都是贪心算法
Prim的贪心策略是加点,而Kruskal的贪心策略是加边
在利用好的数据结构的前提下,两种算法的时间复杂度都是O(|E|log|V|)
Kruskal的空间复杂度为O(|E|),而Prim的空间复杂度为O(|V|²),可以通过题目给的数据范围判断具体用哪种。
Prim
Prim算法的贪心策略为:
选择将不在生成树T中的,离树T最近的点加到T中,并记录所用的边,直到所有的点都在T中为止。
证明可以自己百度。其实我觉得还是比较直观的。所以emmmm不说了。。
挑战上的板子:
int cost[MAX_V][MAX_V] //cost[u][v]表示边e=(u,v)的权值(不存在的情况下设为INF)
int mincost[MAX_V]; //从集合X出发的边到每个顶点的最小权值
bool used[MAX_V]; //顶点i是否包含在集合X中
int V; //顶点数
int prim() {
for(int i = 0; i < V; i++) {
mincost[i] = INF;
used[i] = false;
}
mincost[0] = 0;
int res = 0;
whilee(true) {
int v = -1;
//从不属于X的顶点中选取从X到其权值最小的顶点
for(int u = 0; u < V; u++) {
if(!used[u] && v == -1 || mincost[u] < mincost[v]) v = u;
}
if(v == -1) break;
used[v] = true; //把顶点v加入X
res += mincost[v]; //把边的长度加到结果里
for(int u = 0; u < V; u++) {
mincost[u] = min(mincost[u], cost[u][v]);
}
}
return res;
}
不难发现,Prim和Dijkstra的核心代码很相似,因为他们都是利用点来放缩。
Kruskal
Kruskal的贪心策略为:
找到未判断过的权值最小的边,如果把这条边加入最小生成树中不产生圈,则把它加入到最小生成树中。直到遍历完所有的边,或者已经得到最后的最小生成树为止。
因为Kruskal需要判断一条边的两个点是否在生成树中,所以常常会与并查集一起连用。贪心策略证明略。
挑战上的板子:
struct edge { int u, v, cost; };
bool comp(const edge& e1, const edge& e2) {
return e1.cost < e2.cost;
}
edge es[MAX_E];
int V, E; //顶点数和边数
int kruskal() {
sort(es, es + E, comp); //按照edge.cost的顺序从小到大排列
init_union_find(V); //并查集的初始化
int res = 0;
for(int i = 0; i < E; i++) {
edge e = es[i];
if(!same(e.u, e.v)) {
unite(e.u, e.v);
res += e.cost;
}
}
return res;
}
回到这题,由于这题说路径数没有限制,所以这里不能用kruskal,还是板子题,所以直接贴代码了。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 55
#define inf 0x3f3f3f3f
using namespace std;
int n,m;
int dis[maxn][maxn], vis[maxn], d[maxn];
int prim() {
memset(vis, 0, sizeof(vis));
memset(d, inf, sizeof(d));
vis[1] = 1;
int sum = 0;
for(int i = 1; i <= n; i++) d[i] = dis[1][i];
for(int i = 2; i <= n; i++) {
int v = -1;
for(int j = 1; j <= n; j++) {
if(vis[j]) continue;
if(v == -1 || d[v] > d[j])
v = j;
}
if(v == -1) break;
sum += d[v];
vis[v] = 1;
for(int j = 1; j <= n; j++)
if(!vis[j]) d[j] = min(d[j], dis[v][j]);
}
return sum;
}
int main() {
while(cin>>n) {
if(n == 0) break;
cin>>m;
memset(dis, inf, sizeof(dis));
while(m--) {
int from, to, cost; cin>>from>>to>>cost;
if(dis[from][to] > cost)
dis[from][to] = dis[to][from] = cost;
}
for(int i = 1; i <= n; i++) dis[i][i] = 0;
cout<<prim()<<endl;
}
return 0;
}
D - 缉毒
警察抓贩毒集团。有不同类型的犯罪集团,人员可能重复,集团内的人会相互接触。现在警察在其中一人(0号)身上搜出毒品,认为与这个人直接接触或通过其他人有间接接触的人都是嫌疑犯。问包括0号犯人共有多少嫌疑犯?
Input
多样例输入。
每个测试用例以两个整数n和m开头,其中n为人数,m为犯罪集团数。你可以假定0 < n <= 30000和0 <= m <= 500。在所有的情况下,每个人都有自己独特的整数编号0到n−1, 且0号是公认的嫌疑犯。
接下来m行输入,每个犯罪集团一行。每一行从一个整数k开始,它本身表示集团内成员的数量。按照成员的数量,在这个组中有k个整数表示人员。一行中的所有整数都被至少一个空格隔开。
n = 0且m = 0时输入结束。
Output
对于每个样例,输出嫌疑犯人数。
Sample Input
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
Sample Output
4
1
1
emmmmm这题是之前背包二分并查集专题里面的B题。。。我就直接复制上次写的东西了。。。但是题面有点不同。。。emmmm读者可以自己找之前写的blog。。
问题要判断最终和0号学生一组的同学数目,典型的并查集问题
并查集,是支持以下两种操作的数据结构:合并 :将两个元素合并为一组。查询:返回两个元素是否为一组
首先,我们认为第i个同学是归属于第i组的,即任意两个同学都是不同组的。
然后我们读入每个小组的数据,不断的合并每两个同学所在的组(而不是合并两个同学,因为他们可能已经归属于某个组)
最后就只要扫一遍数出和0号同学一组的同学数目即可。
其实并查集可以看成是一个森林,合并即在森林的两棵树中连一条边使他们连通,查询即判断两个节点所在的树的根节点是否相同。由于并查集只能判断两个元素是否在同一个集合中,而不能判断一个集合究竟有哪些元素,即访问一棵树时只能由下到上访问,而不能从上到下,故减小树的高度会很大程度上提高查询效率。
如何减小树的高度呢?在并查集中,我们只关心某个节点对应的根节点是哪个,而不会关心其父亲或其爷爷是哪个。所以我们在每次取根节点的操作时,都直接将取到的结点连到根节点上就行了。
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 30005
int s[maxn];
int root(int x)// find the root of s[x]
{
if(s[x] == x)
return x;
return s[x] = root(s[x]);
}
int main()
{
int n,m,i,k,t,last,cnt;
while(1)
{
scanf("%d%d",&n,&m);
if(n == 0 && m == 0)//multiple data
break;
for(i = 0; i <= n; i++)//initialize
s[i] = i;
cnt = 0;
for(i = 0; i < m; i++)
{
scanf("%d",&k);//number of groups
last = -1;
while(k--)
{
scanf("%d",&t);
if(last == -1)//the first man of the group
last = t;
else
s[root(t)] = root(last);//unite people of thte group
}
}
t = root(0);
for(i = 0; i < n; i++)//count the answer
if(root(i) == t)
cnt++;
printf("%d\n",cnt);
}
return 0;
}
E - 最短距离
二基楼有n个教室(1≤n≤100),任意两间教室之间可能存在多条路径(一个你所不知道的世界)。作为一个作死的冒险家,你想走遍所有路径以得到任意两间教室之间最短路径的平均值。最后你得到了结果满意地在二基楼迷了路。
Input
多组数据。
每组数据为一行。一组数据包含多个整数对a、b(1≤a, b≤100),表示可以从教室a到教室b。当输入“0 0”时,表示结束该组数据的输入。
最后输入“0 0”,表示整个输入的结束。
Output
每组数据输出一行。以Sample Output的格式输出任意两教室最短距离的平均值,保留3位小数。
Sample Input
1 2 2 4 1 3 3 1 4 3 0 0
1 2 1 4 4 2 2 7 7 1 0 0
0 0
Sample Output
Case 1: average length between pages = 1.833 clicks
Case 2: average length between pages = 1.750 clicks
题意可能第一眼理解不了。首先注意这题是有向图,加入的边都是有向边。题目保证任意两个教室都能够互相到达。所以对于n个教室,不同的<起点,终点>序对一共有 C2n 种。我们要做的是算出这些道路的长度,并求出平均值。比如样例1,一共4个点,所以一共有12条边,他们的长度之和为22,所以平均值为1.833
所以问题明确了,我们要做的是算出任意两点之间的距离,并求和。容易想到是floyd。
这题还有一个问题,就是他给出的教室编号不一定是从1到n的,比如样例2,虽然只有4个教室,但是第7个教室还是有边。
当然,可以用一个数组来储存有哪些点,但是由于这题的n的范围比较小,所以我可以每次都遍历从1到100所有的教室,选择有的教室来进行计算。那怎么判断哪些编号的教室有呢?我们可以在加边的时候把边连接的两个端点的dis[i][i]设置为0,那么最后dis[i][i]=0就表示这个教室存在。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 105
#define inf 0x3f3f3f3f
using namespace std;
int dis[maxn][maxn];
int main() {
int kase = 0;
int a,b;
while(cin>>a>>b) {
if(a == 0 && b == 0) break;
memset(dis, inf, sizeof(dis));
kase++;
do {
if(a == 0 && b == 0) break;
dis[a][b] = 1;
dis[a][a] = dis[b][b] = 0;
}while(cin>>a>>b);
for(int k = 1; k <= 100; k++)
for(int i = 1; i <= 100; i++)
for(int j = 1; j <= 100; j++)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
int cnt = 0, sum = 0;
for(int i = 1; i <= 100; i++) {
if(dis[i][i]) continue;
cnt++;
for(int j = 1; j <= 100; j++) {
if(dis[i][j] > 10000) continue;
sum += dis[i][j];
}
}
printf("Case %d: average length between pages = %.3lf clicks\n", kase, (double)sum/cnt/(cnt-1));
}
return 0;
}
F - 新的目标
时空旅行归来的DY有了新的目标。已知现有的城镇和一些道路,她想再修建一些道路,使任何两个城镇间都直接或间接相连。问最少还需要建设多少条道路?
Input
输入包含若干测试用例。
每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。城镇编号从1道N。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。
Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
Sample Output
1
0
2
998
Huge input, scanf is recommended.
容易想到,这题需要计算的是图中的连通分支数。若图中的连通分支数为x,那么添加x-1条边,即可让他们连通。
求连通分支数可以用染色法,采用BFS遍历。不过对这题来说太麻烦了。
这题可以直接利用并查集,每次加一条边就判断对应的两个端点是否在同一个集合中。如果在,那么不进行操作,否则将两个点合并,合并的结果相当于原来有n个点,合并后只有n-1个点了,即进行一次n–。最后剩下的n就是连通分支数,这样比BFS快近三倍。
emmmm虽然BFS的代码很丑,但是还是贴一下吧。
BFS染色法:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
#define maxn 1005
using namespace std;
int n, m;
int vis[maxn][maxn], color[maxn];
int main() {
while(scanf("%d",&n) != EOF) {
if(n == 0) break;
scanf("%d",&m);
memset(vis, 0, sizeof(vis));
memset(color, 0, sizeof(color));
while(m--) {
int from, to; scanf("%d%d",&from, &to);
vis[from][to] = vis[to][from] = 1;
}
int cnt = 0;
for(int i = 1; i <= n; i++) {
if(color[i]) continue;
cnt++;
queue<int> q; q.push(i);
while(!q.empty()) {
int t = q.front(); q.pop();
color[t] = cnt;
for(int j = 1; j <= n; j++) {
if(color[j] || !vis[t][j]) continue;
q.push(j);
}
}
}
cout<<cnt-1<<endl;
}
return 0;
}
并查集:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 1005
using namespace std;
int n, m;
int pre[maxn];
int find(int idx) {
return idx == pre[idx] ? idx : pre[idx] = find(pre[idx]);
}
int main() {
while(scanf("%d",&n) != EOF) {
if(n == 0) break;
scanf("%d",&m);
for(int i = 1; i <= n; i++) pre[i] = i;
while(m--) {
int a, b; scanf("%d%d",&a, &b);
if(find(a) != find(b)) {
n--;
pre[find(a)] = find(b);
}
}
cout<<n-1<<endl;
}
return 0;
}
G - 不,是你的T-shirt
今年DY姐姐给校赛买漂亮的T袖,从南门到二基楼路漫漫。
假设2030年四川大学重新翻新,校园内存在N个路口,M条路径。问那年帮小姐姐搬T袖的小哥哥们从南门走到二基楼的最短时间是?
Input
每组数据第一行是两个整数N、M(N<=100,M<=10000),N表示四川大学内有几个路口,标号为1的路口是南门所在地,标号为N的路口是二基楼所在地,M则表示在校内有几条路。N=M=0表示输入结束。
接下来M行,每行包括3个整数A,B,C(1<=A,B<=N,1<=C<=1000),表示在路口A与路口B之间有一条路,小哥哥们需要C分钟的时间走过这条路。
输入保证至少存在1条南门到二基楼的路线。
Output
对于每组输入,输出一行,表示小哥哥从南门走到二基楼的最短时间
Sample Input
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
Sample Output
3
2
这题跟A题差不多。单源最短路的板子题。没什么好说的。我只写了dijkstra的版本。我猜bellman也一样可以。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 105
#define maxm 10005
#define inf 0x3f3f3f3f
using namespace std;
int n, m;
int dis[maxn][maxn], vis[maxn], d[maxn];
void dijkstra(int s) {
memset(vis, 0, sizeof(vis));
memset(d, inf, sizeof(d));
d[1] = 0;
while(1) {
int v = -1;
for(int i = 1; i <= n; i++) {
if(vis[i]) continue;
if(v == -1 || d[v] > d[i]) v = i;
}
if(v == -1) break;
vis[v] = 1;
for(int i = 1; i <= n; i++)
d[i] = min(d[i], d[v] + dis[v][i]);
}
}
int main() {
while(cin>>n>>m) {
if(n == 0 && m == 0) break;
memset(dis, inf, sizeof(dis));
for(int i = 1; i <= m; i++) {
int from, to, cost; cin>>from>>to>>cost;
if(dis[from][to] > cost)
dis[from][to] = dis[to][from] = cost;
}
for(int i = 1; i <= n; i++) dis[i][i] = 0;
dijkstra(1);
cout<<d[n]<<endl;
}
return 0;
}
H - 猫的游戏
从前有两只猫。它们实在不知道该出什么题了。
于是它们放弃了治疗,开始玩一个游戏:从乡镇地图中已有的道路里面删除一些道路,并且删除完毕后图仍然是连通的。在所有方案中,删除道路总长度最大的方案为最优方案。
两只猫同时完成了这个游戏。它们都坚信自己是赢家。已知它们的完成方式不同,请判断有没有可能它们的实现方案都是最优的。
Input
第一行是一个整数 t (1 <= t <= 20), 测试用例的数量。每个用例代表一张图,第一行是n和m (1 <= n <= 100), 分别为城镇数和道路数。接下来m行为m个三元组 (xi, yi, wi),表示编号为xi和yi的城镇被长度为wi的道路连接。两个城镇之间最多被一条道路连接。
Output
对于每个用例,如果答案为否定(即不可能都是最优方案),输出最优方案剩余的(注意不是删除的)道路总长度。否则输出字符串 ‘Not Unique!’(不带引号)。
Sample Input
2
3 3
1 2 1
2 3 2
3 1 3
4 4
1 2 2
2 3 2
3 4 2
4 1 2
Sample Output
3
Not Unique!
删除总长度最大,意思也就是MST的长度最小。
要判断赢的方法是不是唯一,实际上就是判断次小生成树和最小生成树是否有相同的权值。
那么问题来了,怎么算次小生成树的权值呢?
算次小生成树的方法是:先算出最小生成树,然后遍历所有的边,尝试把每一条边加入最小生成树中,因为加入后会产生回路,所以同时要删除这个回路中的一条边,当然,我们会删除这个回路上最长的那条边。新得到的这棵树的权值,即为原树的权值,加上加的边的权值,减去删掉的边的权值。
遍历所有的边之后,得到的最小的树即为次小生成树。
我们又发现,其实我们不需要算出真正的次小生成树,只需要在每次加边的时候,判断加的边和删的边权值是否相等。如果相等,那么次小生成树的权值就是最小生成树的权值。
分析到这里,我们要记录的东西包括:最小生成树的权值,最小生成树中从i到j的路径上最长边的权值,以及在最小生成树中用过的边。
要记录这些数据的话,使用kruskal不太方便,所以这里采用prim。确实算法有一丢丢复杂,但是思路清楚的话还是比较容易的。变量的意义在注释中都有。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 105
#define inf 0x3f3f3f3f
using namespace std;
int n,m;
int dis[maxn][maxn], dp[maxn][maxn], mdis[maxn];//dis:距离,dp:最小生成树中从i到j的最小路径上最长的边,mdis:离树最近的距离
int used[maxn][maxn], book[maxn], par[maxn];//used:最小生成树中使用的边,book:已经加入生成树中的点,par:mdis对应的点,即加入点所连的边的另一端
int prim() {
memset(dp,0,sizeof(dp));
memset(book,0,sizeof(book));
memset(used,0,sizeof(used));
book[1] = 1;//将1加入生成树
int sum = 0;
for(int i = 1; i <= n; i++) {
par[i] = 1;//生成树中只有1,加入生成树通过的点只能为1
mdis[i] = dis[1][i];//从点1开始构造生成树,初始化各点到生成树的距离
}
for(int i = 2; i <= n; i++) {//添加n-1个点构成生成树
int minv = -1;
for(int j = 1; j <= n; j++) {//找离生成树最短的点
if(book[j]) continue;
minv = minv == -1 ? j : mdis[j] < mdis[minv] ? j : minv;
}
if(minv == -1) break;
sum += mdis[minv];//更新树的权值
book[minv] = 1;//把最近的结点加入树中
used[minv][par[minv]] = used[par[minv]][minv] = 1;//标记使用了这条边
for(int j = 1; j <= n; j++) {//更新其余点相关属性
if(!book[j]) {//对未访问的点,更新其到最小生成树的距离以及对应的par
if(mdis[j] > dis[j][minv]) {//加入的点为minv,如果到原来树的距离比到minv的距离大,则要更新
mdis[j] = dis[j][minv];//更新最小距离
par[j] = minv;//更新对应的点
}
}
else if(j != minv)//对已经访问的点,要更新它到其他点的路径中的最长边
dp[j][minv] = dp[minv][j] = max(dp[j][par[minv]], mdis[minv]);
}
}
return sum;
}
int main() {
int T; cin>>T;
while(T--) {
memset(dis, inf, sizeof(dis));
cin>>n>>m;
for(int i = 0; i < m; i++) {
int from, to, cost; cin>>from>>to>>cost;
dis[from][to] = dis[to][from] = cost;
}
for(int i = 1; i <= n; i++) dis[i][i] = 0;
int mstCost = prim();
bool flag = false;
for(int i = 1; i <= n && !flag; i++)
for(int j = 1; j <= n && !flag; j++) {
if(i == j || used[i][j]) continue;
if(dis[i][j] == dp[i][j]) flag = true;
}
if(flag) cout<<"Not Unique!"<<endl;
else cout<<mstCost<<endl;
}
return 0;
}