2018SCUACM Training 8 图论

1 篇文章 0 订阅

emmmm居然放题第一天就AK了。。。。

不过主要是有好多题前段时间做过emmmm

要是什么时候做further也能有这个效率就好了emmmm

我好菜啊

比赛链接:2018SCUACM Training 8 图论


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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值