7-1 连通分量 (100 分)
无向图 G 有 n 个顶点和 m 条边。求 G 的连通分量的数目。
输入格式:
第1行,2个整数n和m,用空格分隔,分别表示顶点数和边数, 1≤n≤50000, 1≤m≤100000.
第2到m+1行,每行两个整数u和v,用空格分隔,表示顶点u到顶点v有一条边,u和v是顶点编号,1≤u,v≤n.
输出格式:
1行,1个整数,表示所求连通分量的数目。
输入样例:
在这里给出一组输入。例如:
6 5
1 3
1 2
2 3
4 5
5 6
输出样例:
在这里给出相应的输出。例如:
2
作者 谷方明
单位 吉林大学
代码长度限制 16 KB
时间限制 200 ms
内存限制 10 MB
解法一(DFS):
思路:
本题属于DFS很经典的应用。在主函数当中用一个for循环对每一个结点进行一次查看,若该结点已被访问,则跳过,若未被访问,则将连通分支数 +1 并以其为出发点开始进行DFS,过程中为每一个与其连通的结点打上标记。由于每次从主函数对一个结点进行DFS都会将其所在的整个连通分支的所有结点打上标记,下一次将不会访问已走过结点,这样的话,当且仅当找到一个新的连通分支时,对应的计数器会+1。
细节:
1.此处使用邻接表存储图将会比使用邻接矩阵效率更高。
2.对于打标记的方法:只需要使用一个vis数组用0,1代表对应序号的结点是否被访问过。
3.也可以标记某条边是否走过,只需要在存储边的结构体中(我用的是pair)多加一个标记域。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> map;
int counter = 0;
int m = 0, n = 0;
int* vis;
void dfs(int u) {
vis[u] = 1;//先打标记
for (int v = 0; v < map[u].size(); v++) {//遍历该结点的每个相邻结点
if (vis[map[u][v]]==1) {//访问过则跳过
continue;
}
else {
dfs(map[u][v]);//未访问过则深入
}
}
}
int main(){
scanf("%d%d", &n, &m);
map = vector<vector<int>>(n+1);
int u = 0, v = 0;
vis =new int[n + 1];
for (int i = 0; i < n+1; i++) {
vis[i] = 0;//vis记录每个结点被访问情况
}
for (int i = 0; i < m;i++) {
scanf("%d%d", &u, &v);//存路
map[u].push_back(v);
map[v].push_back(u);
}
for (int i = 1; i < n+1 ; i++) {
if (vis[i]==0) {
counter++;//计数器记录连通分支数
dfs(i);
}
}
printf("%d", counter);
}
解法二(BFS):
思路:
主体框架与解法一相同,只是遍历一个连通分支时的方法由DFS改为BFS,由于二者都可以完整遍历一个连通分支并为结点打上标记,因此效果上没有区别。
代码实现:
略 (可参考第三题的BFS,主体结构一样)
7-2 整数拆分 (100 分)
整数拆分是一个古老又有趣的问题。请给出将正整数 n 拆分成 k 个正整数的所有不重复方案。例如,将 5 拆分成 2 个正整数的不重复方案,有如下2组:(1,4)和(2,3)。注意(1,4) 和(4,1)被视为同一方案。每种方案按递增序输出,所有方案按方案递增序输出。
输入格式:
1行,2个整数n和k,用空格分隔, 1≤k≤n≤50.
输出格式:
若干行,每行一个拆分方案,方案中的数用空格分隔。
最后一行,给出不同拆分方案的总数。
输入样例:
在这里给出一组输入。例如:
5 2
输出样例:
在这里给出相应的输出。例如:
1 4
2 3
2
作者 谷方明
单位 吉林大学
代码长度限制 16 KB
时间限制 100 ms
内存限制 1 MB
解法一(DFS):
思路:
很典型的DFS问题。
解决本题关键有一点:
1.如何保证每种方案按升序输出。
2.如何保证每种方案按方案递增序输出。
对于第一点,每次递归DFS,从等于上一个数开始做选择。
对于第二点,main函数里的DFS从最小值1开始进行选择。
代码实现:
#include <iostream>
int ans[51];
int n = 0,k=0;
int counter = 0;//记录当前解法数
inline void print() {
for (int i = 0; i < k; i++) {//打印结果
printf("%d", ans[i]);
if (i < k - 1) {
printf(" ");
}
}
printf("\n");
}
void dfs(int know, int yu) {
if (know == k) {//若已满足条件,输出
print();
counter++;
return;
}
if (know + 1 == k) {//若还剩最后一步,只有直接填入一种方案
if (yu >= ans[know - 1]) {//若满足则填入,不满足跳过
ans[know] = yu;
dfs(know + 1, 0);
}
}
else {
for (int i = ans[know - 1]; i <= yu; i++) {//保证递增
ans[know] = i;
dfs(know+1, yu-i);//进入下一层DFS
}
}
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) {
ans[0] = i;
dfs(1, n-i);
}
printf("%d", counter);
}
7-3 数字变换 (100 分)
利用变换规则,一个数可以变换成另一个数。变换规则如下:(1)x 变为x+1;(2)x 变为2x;(3)x 变为 x-1。给定两个数x 和 y,至少经过几步变换能让 x 变换成 y.
输入格式:
1行,2个整数x和y,用空格分隔, 1≤x,y≤100000.
输出格式:
第1行,1个整数s,表示变换的最小步数。
第2行,s个数,用空格分隔,表示最少变换时每步变换的结果。规则使用优先级顺序: (1),(2),(3)。
输入样例:
在这里给出一组输入。例如:
2 14
输出样例:
在这里给出相应的输出。例如:
4
3 6 7 14
作者 谷方明
单位 吉林大学
代码长度限制 16 KB
时间限制 100 ms
内存限制 5 MB
解法一(BFS):
思路:
由于是求x变换到y的最短路径,可以联想到求二叉树的最短路径的题目,后者使用BFS和DFS均可(BFS效率更高),但是本题由于可以无穷深入,或者说即使给予限制但分支太多,很可能会导致内存爆掉或者时间超限。因此本题选择BFS更为恰当。
解决本题的关键有两点:
1.如何高效地保存路径。
2.联想到使用BFS的方法。
3.如何满足题目对方法优先级的要求。
对于第一点,我选择使用map<int,int>来保存每一个结点本身的值和其前驱,同时通过find()方法还可以便捷地确定某个结点是否已访问过,若是,则不必再访问,一举两得!
对于第二点,前面已经分析。
对于第三点,BFS时添加新结点的操作顺序按操作优先级从高到低排列。
小坑:
1.之前内存一直爆,一开始以为是保存路径的map有问题,后来尝试过后思考发现是每次新的结点入队都没有考察该结点是否已经访问,由于分支太多,导致队列爆了(可见DFS是死路一条)。。
2.限制条件,每个新结点应该添加一个取值范围,我使用的是<=200000,为什么要添加范围?1.减少无效结点的访问。为什么是200000?(当然,我尝试了100000也可以,不过个人觉得200000更靠谱)1.要考虑到有可能x是沿着值从小到大再到小的路径达到y的,而达到200000后,该条路径必然不可能成为答案了。
代码实现:
#include <iostream>
#include <queue>
#include <map>
#include <string.h>
using namespace std;
queue<int> dl;
map<int, int> pre, depth;
//int* pre = NULL;
inline bool addpre(int pree, int now) {//为新增结点保存前驱信息
if (pre.find(now) == pre.end()) {
pre[now] = pree;
return true;
}
return false;
}
int main() {
bool flag = true;
int x = 0, y = 0, cup = 0, cup2 = 0, depth = 0;
scanf("%d%d", &x, &y);
dl.push(x);
while (!dl.empty()) {
cup = dl.front();//取队首
dl.pop();//删除队首
cup2 = cup + 1;
if (cup2 <= 200000) {//按顺序添加新结点,满足优先级要求
flag=addpre(cup, cup2);//该结点是否已访问
if (cup2 == y) {//到达则跳出
break;
}
if (flag) {未访问则入队
dl.push(cup2);
}
}
cup2 = cup * 2;
if (cup2 <= 200000) {
flag=addpre(cup, cup2);
//adddepth(depth, cup2);
if (cup2 == y) {
break;
}
if (flag) {
dl.push(cup2);
}
}
cup2 = cup - 1;
if (cup2 <= 200000) {
flag=addpre(cup, cup2);
if (cup2 == y) {
break;
}
if (flag) {
dl.push(cup2);
}
}
}
int precup = y; vector<int> ans;//ans保存完整路径
while (precup != x) {//沿着map从y值一直寻找前驱结点并存入ans
ans.push_back(precup);
precup = pre[precup];
}
printf("%d\n", ans.size());//打印路径长度
for (int i = 0; i < ans.size(); i++) {//打印路径
printf("%d", ans[ans.size()-1-i]);
if (i < ans.size() - 1) {
printf(" ");
}
}
}
7-4 旅行 I (100 分)
五一要到了,来一场说走就走的旅行吧。当然,要关注旅行费用。由于从事计算机专业,你很容易就收集到一些城市之间的交通方式及相关费用。将所有城市编号为1到n,你出发的城市编号是s。你想知道,到其它城市的最小费用分别是多少。如果可能,你想途中多旅行一些城市,在最小费用情况下,到各个城市的途中最多能经过多少城市。
输入格式:
第1行,3个整数n、m、s,用空格分隔,分别表示城市数、交通方式总数、出发城市编号, 1≤s≤n≤10000, 1≤m≤100000 。
第2到m+1行,每行三个整数u、v和w,用空格分隔,表示城市u和城市v的一种双向交通方式费用为w , 1≤w≤10000。
输出格式:
第1行,若干个整数Pi,用空格分隔,Pi表示s能到达的城市i的最小费用,1≤i≤n,按城市号递增顺序。
第2行,若干个整数Ci,Ci表示在最小费用情况下,s到城市i的最多经过的城市数,1≤i≤n,按城市号递增顺序。
输入样例:
在这里给出一组输入。例如:
5 5 1
1 2 2
1 4 5
2 3 4
3 5 7
4 5 8
输出样例:
在这里给出相应的输出。例如:
0 2 6 5 13
0 1 2 1 3
作者 谷方明
单位 吉林大学
代码长度限制 16 KB
时间限制 1000 ms
内存限制 10 MB
解法一(Dijkstra):
思路:
直接使用Dijkstra,增加保存途经最大城市数。
解决本题关键有一点:
1.如何正确保存途径最大城市数。
对于第一点,首先要保存途径城市数,则需要在Dijkstra的基础上,每次更新到某个结点的最小代价时,更新其途径城市数为"上一个城市途径城市数+1",而在途径城市数最小的前提下又要保证途径城市数最大,则需要增加操作:在每次更新代价时,若代价相等,则查看新的途径城市数是否大于之前的,若是,则单独对途径城市数进行更新。
小坑:
1.对于堆优化的Dijkstra,不能寻找到最短路径后便结束,必须执行到整个堆为空后才能保证每个城市的途径城市数都为最大。
2.对于朴素的Dijkstra,在访问时即使已寻得最小路径的顶点也不能跳过,才能保证每个城市的途径城市数都为最大。
代码实现:
#include <iostream>
#include <vector>
#include <queue>
#define INF 1<<29;
using namespace std;
int main() {
vector<vector<pair<int, long long>>> tu(10001);
priority_queue<pair<long long,int>,vector<pair<long long, int>>,greater<pair<long long, int>>> dl;//优先队列,优化Dijkstra
int n = 0, m = 0, S = 0, u = 0, v = 0, cost = 0;
scanf("%d%d%d", &n, &m, &S);
for (int i = 0; i < m; i++) {//存图
scanf("%d%d%d", &u, &v,&cost);
tu[u].push_back(make_pair(v, cost));
tu[v].push_back(make_pair(u, cost));
}
long long* dist = new long long[n+1];//代价数组
int* s = new int[n + 1];//已寻得/未寻得集合
int* via = new int[n + 1];//途径城市数
for (int i = 0; i < n + 1; i++) {//初始化
dist[i] = INF;
s[i] = 0;
via[i] = 0;
}
int begin = 0;
int min = 0;
begin = S;
s[begin] = 1;//对出发结点初始化
dist[begin] = 0;
via[begin] = 0;
for (int i = 0; i < tu[begin].size(); i++) {//将出发结点加入集合并更新相应数组
dist[tu[begin][i].first] = tu[begin][i].second;
dl.push(make_pair(tu[begin][i].second,tu[begin][i].first));
via[tu[begin][i].first] = 1;
}
while(!dl.empty()){//执行至优先级队列空
min = dl.top().second;//取最小
dl.pop();
s[min] = 1;//加入集合
for (int i = 0; i < tu[min].size(); i++) {//访问当前结点所有相邻结点并更新信息
if (s[tu[min][i].first] == 0 && dist[tu[min][i].first] > tu[min][i].second + dist[min]) {//如果代价可以更新,同时更新途径城市数
dist[tu[min][i].first] = tu[min][i].second + dist[min];
dl.push(make_pair(dist[tu[min][i].first], tu[min][i].first));
via[tu[min][i].first] = via[min] + 1;
}
else if (s[tu[min][i].first] == 0 && dist[tu[min][i].first] == tu[min][i].second + dist[min]&& via[tu[min][i].first] < via[min] + 1) {//如果代价相等,考察途径城市数是否可以更新
via[tu[min][i].first] = via[min] + 1;
}
}
}
for (int i = 1; i < n+1; i++) {//输出
printf("%d", dist[i]);
if (i < n) {
printf(" ");
}
}
printf("\n");
for (int i = 1; i < n + 1; i++) {
printf("%d", via[i]);
if (i < n) {
printf(" ");
}
}
}
总结:
1.要注意算法应用在各种算法题情境下的细节差异。
2.注意减少无效计算,免得内存爆或者超时。