🏆🏆🏆🏆🏆🏆🏆
欢迎观看我的博客,如有问题交流,欢迎评论区留言,一定尽快回复!(大家可以去看我的专栏,是所有文章的目录)
文章字体风格:
红色文字表示:重难点✔★
蓝色文字表示:思路以及想法✔★
如果大家觉得有帮助的话,感谢大家帮忙
点赞!收藏!转发!
我的qq号是:1210931886,欢迎大家加群,一起学习,互相交流,共同进步🎉🎉✨✨
🥇🥇🥇🥇🥇🥇🥇
蓝桥杯系列,为大家提供
- 做题全集,备战蓝桥杯,就做这个系列的题即可
- 一个大概的做题规划——大家最好在此基础上提前两个月准备
备战蓝桥杯就刷这些题
第一天博客链接
第二天博客链接
蓝桥杯 刷题全集
- 1. 排列数字(3分钟)
- 2. n-皇后问题
- 1. 走迷宫
- 二刷总结(队列存储一个节点pair<int,int>)
- 三刷总结 走过的点标记上距离(既可以记录距离,也可以判断是否走过)
- ★ ★ 例题2. 八数码
- 二刷总结
- 1. 树的重心
- 二刷总结( 注意无向图的遍历 )
- 1. 图中点的层次( 无权最短路 )
- 1. 有向图的拓扑排序 ✔12.24
- 做题总结
- 1. Dijkstra求最短路 I(邻接矩阵)✔12.24
- 二刷总结
- ★ 1. Dijkstra求最短路 II(邻接表)✔12.24
- 二刷总结
- 模板 (遍历所有边只走一步)
- 1. 有边数限制的最短路 ✔ 12.24
- 做题总结:
- 1. spfa求最短路 ✔12.24
- 做题总结:
- 例题 spfa判断负环 ✔12.26
- 刷题总结
- 1. Floyd求最短路 ✔12.26
- 做题总结
- Prim算法求最小生成树 ✔12.27
- 做题总结
1. 排列数字(3分钟)
每次遍历dfs参数是 遍历的坑位
原题链接
#include<iostream>
using namespace std;
const int N = 10;
int path[N];//保存序列
int state[N];//数字是否被用过
int n;
void dfs(int u)//u表示的是 第几个坑位
{
if(u > n)//数字填完了,输出
{
for(int i = 1; i <= n; i++)//输出方案
cout << path[i] << " ";
cout << endl;
}
for(int i = 1; i <= n; i++)//空位上可以选择的数字为:1 ~ n
{
if(!state[i])//如果数字 i 没有被用过
{
path[u] = i;//放入空位
state[i] = 1;//数字被用,修改状态
dfs(u + 1);//填下一个位
state[i] = 0;//回溯,取出 i
}
}
}
int main()
{
cin >> n;
dfs(1);
}
2. n-皇后问题
原题链接
方法 1. 按行遍历(过程中有回溯、剪枝)
思想:
- 每次递归中,遍历一行的元素,如果可以放皇后,就递归到下一行,下一行中不行了,就返回来,回溯,
//cpp
#include <iostream>
using namespace std;
const int N = 11;
char q[N][N];//存储棋盘
bool dg[N * 2], udg[N * 2], cor[N];
//点对应的两个斜线以及列上是否有皇后
int n;
void dfs(int r)
{
if(r == n)//放满了棋盘,输出棋盘
{
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
cout << q[i][j];
cout << endl;
}
cout << endl;
return;
}
for(int i = 0; i < n; i++)//第 r 行,第 i 列 是否放皇后
{
if(!cor[i] && !dg[i + r] && !udg[n - i + r])//不冲突,放皇后
{
q[r][i] = 'Q';
cor[i] = dg[i + r] = udg[n - i + r] = 1;//对应的 列, 斜线 状态改变
dfs(r + 1);//处理下一行
cor[i] = dg[i + r] = udg[n - i + r] = 0;//恢复现场
q[r][i] = '.';
}
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ )
q[i][j] = '.';
dfs(0);
return 0;
}
方法2. 按每个元素遍历(没有减枝)
// 不同搜索顺序 时间复杂度不同 所以搜索顺序很重要!
#include <iostream>
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool row[N], col[N], dg[N], udg[N];
// 因为是一个个搜索,所以加了row
// s表示已经放上去的皇后个数
void dfs(int x, int y, int s)
{
// 处理超出边界的情况
if (y == n) y = 0, x ++ ;
if (x == n) { // x==n说明已经枚举完n^2个位置了
if (s == n) { // s==n说明成功放上去了n个皇后
for (int i = 0; i < n; i ++ ) puts(g[i]);
puts("");
}
return;
}
//和上面按行遍历的差别就是,这里没有循环
// 分支1:放皇后
if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]) {
g[x][y] = 'Q';
row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
dfs(x, y + 1, s + 1);
row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
g[x][y] = '.';
}
// 分支2:不放皇后
dfs(x, y + 1, s);
}
int main() {
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ )
g[i][j] = '.';
dfs(0, 0, 0);
return 0;
}
1. 走迷宫
二刷总结(队列存储一个节点pair<int,int>)
三刷总结 走过的点标记上距离(既可以记录距离,也可以判断是否走过)
- bfs 需要队列
- ==走过的点标记上距离(既可以记录距离,也可以判断是否走过)==没走过的置为-1
- (队列存储一个节点pair<int,int>)
原题链接
原题链接
#include<iostream>
#include<cstring>
using namespace std;
int n,m;
int p[110][110];
int d[110][110];
int dx[4] = {-1,0,1,0};
int dy[4] = {0,-1,0,1};
pair<int,int> q[110*110];
int hh,tt=-1;
void bfs()
{
q[++tt] = {1,1};
p[1][1] = 1;
d[1][1] = 0;
while(hh<=tt)
{
pair<int,int> item = q[hh++];
for(int i = 0; i < 4; i++)
{
int x = item.first;
int y = item.second;
//cout << x << ' ' << y << endl;
if(p[x+dx[i]][y+dy[i]]!=1 && x+dx[i]<=n && x+dx[i] > 0 && y+dy[i] <=m && y+dy[i] >0)
{
d[x+dx[i]][y+dy[i]] = d[x][y] + 1;
q[++tt] = {x+dx[i],y+dy[i]};
p[x+dx[i]][y+dy[i]] = 1;
}
}
}
}
int main()
{
cin >> n >> m;
for(int i = 1; i<=n; i++)
{
for(int j = 1; j<=m; j++)
cin >> p[i][j];
}
bfs();
cout << d[n][m];
return 0;
}
★ ★ 例题2. 八数码
原题链接
二刷总结
- 字符串存储二维数组
- 每种情况对应一个移动距离,正好就用map
- 字符串有find函数,map有count函数
- map的count就可以判别 该位置是否走过
补充一点就是,一种情况只能走过一次!不可能走走走地出现与之前一样的情况,这不就是白走了嘛- 先swap才能接着判断
#include<iostream>
#include<unordered_map>
#include<cstring>
#include<queue>
using namespace std;
int bfs(string s)
{
queue<string> q;
q.push(s);
int dx[4] = { 1,0,-1,0 },
dy[4] = { 0,-1,0,1 };
unordered_map<string, int> d;
d[s] = 0;
while (!q.empty())
{
string f = q.front();
q.pop();
if (f == "12345678x")
return d[f];
int df = d[f];
int k = f.find("x");
int x = k / 3;
int y = k % 3;
for (int i = 0; i < 4; i++)
{
if (x + dx[i] <= 2 && x + dx[i] >= 0 && y + dy[i] <= 2 && y + dy[i] >= 0)
{
swap(f[k], f[(x + dx[i]) * 3 + y + dy[i]]);
if (!d.count(f))
{
d[f] = df + 1;
q.push(f);
}
swap(f[k], f[(x + dx[i]) * 3 + y + dy[i]]);
}
}
}
return -1;
}
int main()
{
char ch;
string s;
for (int i = 0; i < 9; i++)
{
cin >> ch;
s += ch;
}
cout << bfs(s);
return 0;
}
1. 树的重心
原题链接
二刷总结( 注意无向图的遍历 )
- 无向图的节点数 是 2倍
- 无向图的连接
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
- 遍历过的点标记一下,不再遍历,因为无向图可能往回遍历
#include<iostream>
#include<cstring>
using namespace std;
const int N = 2e5 + 10;
int h[N], e[N], ne[N], idx;
bool st[N];
int sum;
int n;
int res;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int q = 0;
int dfs(int x)
{
if (x == -1)return 0;
st[x] = true;
int xx = 0;
int xxx = 0;
for (int i = h[x]; i != -1; i = ne[i])
{
if (st[e[i]] == false)
{
int s = dfs(e[i]);
xx = max(s, xx);
xxx += s;
}
}
//cout << e[x] << "下面的值" << xxx << endl;
sum = max(xx, n - xxx - 1);
if (sum < res)
{
res = sum;
q = x;
}
return xxx + 1;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n;
int a, b;
while (cin >> a >> b)
add(a, b), add(b, a);
res = n;
dfs(1);
cout << res;
return 0;
}
1. 图中点的层次( 无权最短路 )
原题链接
原题链接
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
int e[N], ne[N], h[N], idx;
int d[N];
bool st[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void bfs(int x)
{
queue<int> q;
q.push(x);
while (q.size())
{
int u = q.front();
q.pop();
st[u] = true;
int dd = d[u];
for (int i = h[u]; i != -1; i = ne[i])
{
//这里可以改进,因为可以用d 判断是否遍历过该节点
if (st[e[i]] == false)
{
st[e[i]] = true;
q.push(e[i]);
d[e[i]] = min(d[e[i]], dd + 1);
}
}
}
}
int main()
{
memset(h, -1, sizeof h);
memset(d, 0x3f, sizeof d);
d[1] = 0;
int n, k;
cin >> n >> k;
for (int i = 1; i <= k; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
}
bfs(1);
if(d[n] != 0x3f3f3f3f)
cout << d[n];
else
cout << -1;
return 0;
}
1. 有向图的拓扑排序 ✔12.24
做题总结
- 拓扑是一个宽搜
- 遍历顺序是 度为0(可能有多个为0的)
- 可以用q[] 表示队列,这样就用一个队列就可以存储拓扑的结果和 遍历的过程了(也就是拓扑排序的遍历过程,就是答案顺序)
原题链接
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int h[N],e[N],ne[N],idx;
int n,m;
int q[N],d[N];//q表示队列,d表示点的入度
void add(int a,int b)
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
bool topsort()
{
int hh=0,tt=-1;
for(int i=1;i<=n;i++)
if(!d[i])
q[++tt]=i;//将入度为零的点入队
while(hh<=tt)
{
int t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
d[j]--;//删除点t指向点j的边
if(d[j]==0)//如果点j的入度为零了,就将点j入队
q[++tt]=j;
}
}
return tt==n-1;
//表示如果n个点都入队了话,那么该图为拓扑图,返回true,否则返回false
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));//如果程序时间溢出,就是没有加上这一句
for(int i=0;i<m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);//因为是a指向b,所以b点的入度要加1
d[b]++;
}
if(topsort())
{
for(int i=0;i<n;i++)
printf("%d ",q[i]);
//经上方循环可以发现队列中的点的次序就是拓扑序列
//注:拓扑序列的答案并不唯一,可以从解析中找到解释
puts("");
}
else
puts("-1");
return 0;
}
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1e5+10;
bool st[N];
int e[N],ne[N],idx,h[N];
int b[N];//每个节点的入读
int n,k;
queue<int> q,ans;
void bfs()
{
while(q.size())
{
int f = q.front();
q.pop();
for(int i = h[f]; i != -1; i = ne[i])
{
b[e[i]]--;
if(b[e[i]]==0)
{
ans.push(e[i]);
q.push(e[i]);
}
}
}
}
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
int main()
{
memset(h,-1,sizeof h);
cin >> n >> k;
for(int i = 1; i <= k; i++)
{
int a,bb;
cin >> a >> bb;
add(a,bb);
b[bb]++;
}
for(int i = 1; i <= n; i++)
{
if(b[i]==0)
{
//cout << i << endl;
q.push(i);
ans.push(i);
}
}
bfs();
if(ans.size()!=n)
{
cout << -1;
return 0;
}
//cout << ans.size() << endl;
while(ans.size())
{
cout << ans.front() << ' ';
ans.pop();
}
return 0;
}
1. Dijkstra求最短路 I(邻接矩阵)✔12.24
原题链接
刷题总结
- 稀疏矩阵 和 疏密矩阵(疏密矩阵 可以用 邻接矩阵存储方式)
- 邻接矩阵直接就可以存储 边距离了
- d存储到1节点的距离
二刷总结
- dijk就是两步 :1. 在dist里选出最小值 2.遍历dist更新
如何选(如下)
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
★ 1. Dijkstra求最短路 II(邻接表)✔12.24
原题链接
二刷总结
- 利用小根堆存储,元素是PII
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int,int> PII;
const int N = 1e6+10;
int e[N],ne[N],h[N],idx;
int w[N];
int d[N];
bool st[N];
int n,m;
void add(int a,int b,int c)
{
e[idx] = b,ne[idx] = h[a],w[idx] = c,h[a] = idx++;
}
void dijkstra()
{
priority_queue<PII, vector<PII>, greater<PII>> heap;
d[1] = 0;
heap.push({0,1});
while(heap.size())
{
auto item = heap.top();
heap.pop();
if(st[item.second]==true)continue;
st[item.second]=true;
for(int i = h[item.second]; i!= -1; i = ne[i])
{
if(d[e[i]] > w[i] + item.first)
{
d[e[i]] = w[i] + item.first;
heap.push({d[e[i]],e[i]});
}
}
}
}
int main()
{
memset(d,0x3f,sizeof d);
memset(h,-1,sizeof h);
cin >> n >> m;
for(int i = 0; i < m; i++)
{
int a,b,c;
cin >> a >> b >> c;
add(a,b,c);
}
dijkstra();
if(d[n]!=0x3f3f3f3f)
cout << d[n];
else
cout << -1;
return 0;
}
模板 (遍历所有边只走一步)
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
struct Edge // 边,a表示出点,b表示入点,w表示边的权重
{
int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i = 0; i < n; i ++ )
{
for (int j = 0; j < m; j ++ )
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
if (dist[b] > dist[a] + w)
dist[b] = dist[a] + w;
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
1. 有边数限制的最短路 ✔ 12.24
原题链接
做题总结:
- dijkstra不能处理 负权边
- 由于有k的限制,所以每次遍历一条边的时候,需要用备份数据,因为可能会发生数据改变(这样可能导致一次遍历所有边直接到n点了)
- 有可能出现n节点连接的是负边,那么我们就让n节点的值 > 0x3f3f3f3f/2 这样就可以了
#include<iostream>
#include<cstring>
using namespace std;
const int N = 510, M = 10010;
struct Edge {
int a;
int b;
int w;
} e[M];//把每个边保存下来即可
int dist[N];
int back[N];//备份数组防止串联
int n, m, k;//k代表最短路径最多包涵k条边
int bellman_ford() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i++) {//k次循环
memcpy(back, dist, sizeof dist);
for (int j = 0; j < m; j++) {//遍历所有边
int a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], back[a] + w);
//使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
else return dist[n];
}
int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i++) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
e[i] = {a, b, w};
}
int res = bellman_ford();
if (res == -1) puts("impossible");
else cout << res;
return 0;
}
1. spfa求最短路 ✔12.24
原题链接
做题总结:
- 和宽搜差不多,只是可能会 返回走(但距离值更新了,就把这个节点入队列再处理一次)
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1e5+10;
int e[N],ne[N],h[N],idx;
int w[N];
bool st[N];
int n,m;
void add(int x,int y,int c)
{
e[idx] = y,ne[idx] = h[x],w[idx] = c,h[x] = idx++;
}
int d[N];
void spfa()
{
memset(d,0x3f,sizeof d);
d[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
auto f = q.front();
q.pop();
st[f] = false;
for(int i = h[f]; i != -1; i = ne[i])
{
int j = i;
if(d[e[j]] > d[f] + w[j])
{
d[e[j]] = d[f] + w[j];
if(st[e[j]]==false)
{
q.push(e[j]);
st[e[j]] = true;
}
}
}
}
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
for(int i = 0; i < m; i++)
{
int x,y,c;
cin >> x >> y >> c;
add(x,y,c);
}
spfa();
if(d[n] == 0x3f3f3f3f)
cout << "impossible";
else
cout << d[n];
return 0;
}
原题链接
- 什么是负环
图1中:2 到 3 到 4 到 2 路径长度为 -10
图2中:2 到 3 到 4 到 2 路径长度为 10
图1才叫负环
图2不是负环
-
出现负环会怎么样
但出现负环的时候,如果我们要去求1到n的最短路,那么过程中,一定会在这个负环中一直转圈,导致路程可以变为负无穷 -
怎么判断图中是否有负环?
综上,我们就采取求最小路径的方式(但是本题不是求最短路),当我们求最短路径的过程中,发现有一段路径重复走,那么就说明一定出现了负环
问题来了:怎么判断某段路径在重复走
我们想,1到n号点 最多才可能走了n-1条边
如果我们发现 到某点时 已经走了 大于等于n条边,那么一定就是有负环了
由于我们不知道 1 到 x点最多可能有多少条边,但一定不会超过 n - 1 条边,所以我们就都用 大于等于n条边去判断
例题 spfa判断负环 ✔12.26
原题链接
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool spfa()
{
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
st[i] = true;
q.push(i);
}
while (q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
if (spfa()) puts("Yes");
else puts("No");
return 0;
}
刷题总结
- e,ne,h,idx 用于存储边,所以数值应该与边一样多
- 把所有点都入队列,防止不是连通图
- dist里存储多少都可以,因为我们只需判断负权回路
- 当一个点所走的路径长度大于n,那么就一定有负边,因为最多就是n正常的话。
- 一定要有st数组,判断是否再走这个点
1. Floyd求最短路 ✔12.26
原题链接
做题总结
- 用二维数组存储更方便
- 读入存储的时候,读取最小值,并且到自身值为0
- Floyd
#include<iostream>
#include<cstring>
using namespace std;
int n,m,k;
const int N = 210;
int d[N][N];
void Floyd()
{
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
d[i][j] = min(d[i][j],d[i][k]+d[k][j]);
}
int main()
{
cin >> n >> m >> k;
memset(d,0x3f,sizeof d);
for(int i = 0; i < m; i++)
{
int x,y,c;
cin >> x >> y >> c;
d[x][y] = min(d[x][y], c);
d[x][x] = 0;
d[y][y] = 0;
}
Floyd();
for(int i = 0; i < k; i++)
{
int x,y;
cin >> x >> y;
if(d[x][y]>=0x3f3f3f3f/2)
cout << "impossible" << endl;
else
cout << d[x][y] << endl;
}
return 0;
}
Prim算法求最小生成树 ✔12.27
原题链接
做题总结
- prim算法的思路:选出每个最小边且该节点没有走过。
关键开辟一个集合存储每个点的所遍历过的最小边的值,(有个前提就是,需要一个点一个点地走,所以有了prim算法)- 自己到自己的距离也应是0x3f,因为d存储的是自己的最短边
- 图存储的 都是最小值
#include<iostream>
#include<cstring>
using namespace std;
const int N = 510;
int p[N][N], d[N];
int n, m;
bool st[N];
int res;
int sum;
void prim()
{
memset(d, 0x3f, sizeof d);
d[1] = 0;
for (int k = 0; k < n; k++)
{
int t = -1;
for (int l = 1; l <= n; l++)
{
if (st[l] == false && (t == -1 || d[t] > d[l]))
t = l;
}
if (d[t] == 0x3f3f3f3f)
{
res++;
return;
}
st[t] = true;
sum += d[t];
for (int i = 1; i <= n; i++)
{
d[i] = min(d[i], p[t][i]);
}
}
}
int main()
{
memset(p,0x3f,sizeof p);
cin >> n >> m;
for (int i = 0; i < m; i++)
{
int x, y, c;
cin >> x >> y >> c;
p[x][y]= p[y][x] = min(p[x][y],c);
}
prim();
if (res)
cout << "impossible";
else
cout << sum;
return 0;
}