宽搜问题最重要的是可以寻找最短路(当边权相同时)。深搜保证可以搜到结果但是不一定是最短路!
当边权值不相同时,选择其他最短路算法。
从左上角走到右下角最短路径长度
#include
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int g[N][N], d[N][N];
int n, m;
int bfs()
{
queue<PII> q;
q.push({0, 0});
memset(d, -1, sizeof d);
d[0][0] = 0;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}; //上 下 左 右
while(!q.empty())
{
PII t = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
int tx = t.first + dx[i];
int ty = t.second + dy[i];
//cout << tx << " " << ty << endl;
if(tx >= 0 && tx < n && ty >= 0 && ty < m && g[tx][ty] == 0 && d[tx][ty] == -1)
{
d[tx][ty] = d[t.first][t.second] + 1;
//cout << tx << " " << ty << endl;
q.push({tx, ty});
}
}
}
return d[n - 1][m - 1];
}
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
cin >> g[i][j];
cout << bfs();
return 0;
}
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}; //上 下 左 右
typedef pair PII; q.push({tx, ty});
d[tx][ty] = d[t.first][t.second] + 1;
思路是:将每种情况的小方格看成是一个状态,那么转化为已知开始、结束状态和转移方式,求转移的最小步骤?
几个难点:
#include
using namespace std;
int bfs(string start)
{
string end = "12345678x";
queue<string> q;
unordered_map<string, int> d;
q.push(start);
d[start] = 0;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
while(!q.empty())
{
string t = q.front();
q.pop();
int dis = d[t];
if(t == end)
return dis;
//状态转移
for(int i = 0; i < 4; i++)
{
int k = t.find('x');
int tx = k / 3, ty = k % 3;
int a = tx + dx[i], b = ty + dy[i];
if(a < 3 && a >= 0 && b < 3 && b >= 0)
{
swap(t[k], t[a * 3 + b]);
if(!d.count(t))
{
d[t] = dis + 1;
q.push(t);
}
swap(t[k], t[a * 3 + b]);
}
}
}
return -1;
}
int main()
{
char ch;
string start;
for(int i = 0; i < 9; i++)
{
cin >> ch;
start += ch;
}
cout << bfs(start);
return 0;
}
题目很简单,找到1到n的最短路径(边权为1),还是和走迷宫一样的遍历,只不过存储换成了图。
#include
using namespace std;
const int N = 1e5 + 10;
int n, m, a, b;
int d[N];
int h[N], e[N], ne[N], idx;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
int bfs()
{
queue<int> q;
q.push(1);
memset(d, -1, sizeof d);
d[1] = 0;
while(!q.empty())
{
int t = q.front();
q.pop();
for(int i = h[t]; i != -1; i = ne[i])
{
if(d[e[i]] == -1)
{
q.push(e[i]);
d[e[i]] = d[t] + 1;
}
}
}
return d[n];
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while(m--)
{
cin >> a >> b;
add(a, b);
}
cout << bfs();
return 0;
}
int h[N], e[N], ne[N], idx;
`void add(int a, int b){e[idx] = b; ne[idx] = h[a]; h[a] = idx;idx++;}for(int i = h[t]; i != -1; i = ne[i])
#include
using namespace std;
const int N = 1e5 + 10;
const int M = N * 2;
int n, a, b, ans = N;
int h[N], e[M], ne[M], idx;
bool st[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
//返回以u为根的子树数量和
int dfs(int u)
{
st[u] = true;
int sum = 1; //包含u在内的下面子树的数量和
int res = 0; //以u为根的子树数量的最大值
for(int i = h[u]; i != -1; i = ne[i])
{
int p = e[i];
if(!st[p])
{
int s = dfs(p);
res = max(res, s);
sum += s;
}
}
res = max(res, n - sum);//父节点的那个子树数量
ans = min(ans, res);
return sum;
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i++)
{
cin >> a >> b;
add(a, b);
add(b, a);
}
dfs(1);
cout << ans;
return 0;
}
有向图的拓扑序列,判断DAG图是否存在环?(BFS的应用)
有向无环图一定存在拓扑序列。
箭头都是从前往后指的,入度为0说明没有节点指向它,排在最前面
#include
using namespace std;
const int N = 1e5 + 10;
int n, m, a, b;
int h[N], e[N], ne[N], idx, d[N];
int q[N];
int hh, tt;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
bool toposort()
{
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 p = e[i];
d[p]--;
if(!d[p])
q[++tt] = p;
}
}
if(tt == n - 1)
return true;
else
return false;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while(m--)
{
cin >> a >> b;
add(a, b);
d[b]++;
}
if(toposort())
{
for(int i = 0; i <= tt; i++)
cout << q[i] << " ";
}
else
cout << -1;
return 0;
}
数组模拟队列
hh = 0, tt = -1;
q[++tt] = num;
tmp = q[hh++];
hh <= tt
这个简单代码是把第一个节点自动加入st数组
dist[1] = 0; 其他为INF
#include
using namespace std;
const int N = 510;
const int M = 1e5 + 10;
int n, m, a, b, c;
int g[N][N], dist[N], st[N];
//可以实现从第一个点开始自动加入st中
int dijkstra()
{
//初始化
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
//迭代n次,将n个点加入st中
for(int i = 0; i < n; i++)
{
//遍历dist数组,找到不在st数组中距离最小的节点midx
int midx = -1;
for(int j = 1; j <= n; j++)
{
if(!st[j] && (midx == -1 || dist[midx] > dist[j]))
midx = j;
}
st[midx] = true;
for(int j = 1; j <= n; j++)
dist[j] = min(dist[j], dist[midx] + g[midx][j]); //松弛dist数组
}
//无法到达n节点
if(dist[n] == 0x3f3f3f3f)
return -1;
else
return dist[n];
}
int main()
{
scanf("%d %d", &n, &m);
memset(g, 0x3f, sizeof g);
while(m--)
{
scanf("%d %d %d", &a, &b, &c);
g[a][b] = min(g[a][b], c); //存在重边
}
cout << dijkstra();
return 0;
}
暂时还不是很清晰,我还需要再缕缕
#include
using namespace std;
const int N = 150010;
int h[N], e[N], ne[N], w[N], idx;
int n, m, a, b, c;
int st[N], dist[N];
typedef pair<int, int> PII;
void add(int a, int b, int c)
{
e[idx] = b; ne[idx] = h[a]; h[a] = idx; w[idx] = c; idx++;
}
int dijkstra()
{
priority_queue<PII, vector<PII>, greater<PII>> heap;
memset(dist, 0x3f, sizeof dist);
heap.push({0, 1});//dist[0] = 1;
while(!heap.empty())
{
//heap中最多有m个元素,复杂度为mlogm
auto t = heap.top();
heap.pop();
int distance = t.first, ti = t.second;
if(st[ti]) continue;
st[ti] = true;
//对每一个头都进行了遍历,相当于遍历了所有边m,最多push了m次,外层while循环最多m次
for(int i = h[ti]; i != -1; i = ne[i])
{
int j = e[i];
if(distance + w[i] < dist[j]) //只有产生距离更小的点时才去更新j,减少堆中的数
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if(dist[n] == 0x3f3f3f3f)
return -1;
else
return dist[n];
}
int main()
{
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
while(m--)
{
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
}
cout << dijkstra();
return 0;
}
我感觉bf算法有点像暴力(带负权值),dijkstra像是贪心选择最小的(非负权值)。
算法流程O(nm):
dist[b] = min(dist[b], backup[a] + w);
#include
#include
using namespace std;
const int N = 510;
const int M = 10010;
int n, m, k, a, b, w;
int dist[N], backup[N];
struct edge
{
int a, b, w;
}e[M];
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < k; i++)
{
memcpy(backup, dist, sizeof dist);
for(int j = 0; j < m; j++)
{
a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], backup[a] + w);
}
}
return dist[n];
}
int main()
{
scanf("%d %d %d", &n, &m, &k);
for(int i = 0; i < m; i++)
scanf("%d %d %d", &e[i].a, &e[i].b, &e[i].w);
int ans = bellman_ford();
if(ans > 0x3f3f3f3f / 2)
puts("impossible");
else
cout << ans;
return 0;
}
这个题有负环没有关系是因为有k次数限制
其他点:
spfa算法是对bf算法的优化,dist[b] = min(dist[b], backup[a] + w);
只有在backup[a]更新了,dist[b]才会更新。因此选择BFS对其进行优化,把变小了的backup[a]放进队列,然后取出更新其他的。
此题不能存在负环,存在负环可能会无限循环下去
#include
#include
#include
using namespace std;
const int N = 100010;
int n, m, a, b, ww;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], st[N];
void add(int a, int b, int ww)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
w[idx] = ww;
idx++;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;//表示是否在队列中
while(!q.empty())
{
a = q.front();
q.pop();
st[a] = false;//spfa可以多次更新,多次加入队列
for(int i = h[a]; i != -1; i = ne[i])
{
b = e[i];
ww = w[i];
if(dist[a] + ww < dist[b])
{
dist[b] = min(dist[b], dist[a] + ww);
if(!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
return dist[n];
}
int main()
{
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
while(m--)
{
scanf("%d %d %d", &a, &b, &ww);
add(a, b, ww);
}
int t = spfa();
if(t == 0x3f3f3f3f)//加入队列的点都是从起点开始的,不会存在 不在最短路中且更新的dist[n]这种情况
puts("impossible");
else
cout << t;
return 0;
}
抽屉原理
如果经过了多于n条边,说明存在环,因为只有n-1条边
环存在的原因是最短路变小了,那环就是负环
cnt[b]表示从1到b中经过了多少条边,cnt[b] = cnt[a] + 1;//1是边a->b
#include
#include
#include
using namespace std;
const int N = 100010;
int n, m, a, b, ww;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], st[N], cnt[N];
void add(int a, int b, int ww)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
w[idx] = ww;
idx++;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;//可以不初始化
queue<int> q;
//加入所有节点
for(int i = 1; i <= n; i++)
{
st[i] = true;
q.push(i);
}
while(!q.empty())
{
a = q.front();
q.pop();
st[a] = false;
for(int i = h[a]; i != -1; i = ne[i])
{
b = e[i];
ww = w[i];
if(dist[a] + ww < dist[b])
{
dist[b] = min(dist[b], dist[a] + ww);
cnt[b] = cnt[a] + 1; //cnt更新
if(cnt[b] >= n)//判断是否存在负环
return true;
if(!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
return false;
}
int main()
{
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
while(m--)
{
scanf("%d %d %d", &a, &b, &ww);
add(a, b, ww);
}
if(spfa()) puts("Yes");
else puts("No");
return 0;
}
评论里的一些问题:
dist[b] < dist[a] + w
时,才会cnt++,不初始化更新只可能是w < 0,从负边出现才开始更新统计cnt,由于如果有负环的话,一定会无数次更新,一定会导致cnt>=n的,所以不必在意是否初始化多源最短路径 每两个点之间的最短路径
算法思路:三重循环
d[i][j] = min(d[i][j], d[i][k] + d[k][j])
原理是个动态规划
d[k][i][j] = min(d[k][i][j], d[k - 1][i][k] + d[k - 1][k][j]
d[k][i][j]是指使用前k个节点中i和j的最短路径,可以使用前k-1个更新d[i][j] = min(d[i][j], d[i][k] + d[k][j])
不使用之前的#include
using namespace std;
const int N = 210;
const int INF = 1e9;
int n, m, k, a, b, w;
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()
{
scanf("%d %d %d", &n, &m, &k);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(i == j) d[i][j] = 0;
else d[i][j] = INF;
while(m--)
{
scanf("%d %d %d", &a, &b, &w);
d[a][b] = min(d[a][b], w);
}
floyd();
while(k--)
{
scanf("%d %d", &a, &b);
if(d[a][b] > INF / 2) puts("impossible");//存在负权边,可能导致不是最短路但是距离减少
else cout << d[a][b] << endl;
}
return 0;
}
朴素版Prim算法
算法原理和dikstra很像,差别在于dist数组含义不同(dij:到1号点的距离,prim到集合的距离)
dist[j] = min(dist[j], g[t][j])
#include
#include
using namespace std;
const int N = 510;
const int INF = 0x3f3f3f3f;
int g[N][N];
int dist[N], st[N];
int n, m, u, v, w;
int prim()
{
//初始化为INF,选择第一个点为1
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
int ans = 0;
//选择最小的点加入
for(int i = 0; i < n; i++)//选n个点
{
int t = 0;
for(int j = 1; j <= n; j++)
{
if(!st[j] && dist[j] < dist[t])
t = j;
}
if(dist[t] == INF)//最小的是INF,全是INF,非连通
return INF;
ans += dist[t];
st[t] = true;
//更新到集合的距离
for(int j = 1; j <= n; j++)
{
if(!st[j] && g[t][j] < dist[j])
dist[j] = g[t][j];
}
}
return ans;
}
int main()
{
scanf("%d %d", &n, &m);
memset(g, 0x3f, sizeof g);
while(m--)
{
scanf("%d %d %d", &u, &v, &w);
g[u][v] = min(g[u][v], w);
g[v][u] = min(g[v][u], w);
}
int t = prim();
if(t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}
堆优化版prim算法
和堆优化版的dijkstra差不多,不过用cnt统计多少个点加入了mst
#include
#include
#include
using namespace std;
const int N = 510;
const int INF = 0x3f3f3f3f;
int g[N][N];
int dist[N], st[N];
int n, m, u, v, w;
typedef pair<int, int> PII;
int prim()
{
priority_queue<PII, vector<PII>, greater<PII>> q;
q.push({0, 1});
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
int ans = 0, cnt = 0;
while(q.size())//选n个点
{
auto t = q.top();
q.pop();
int distance = t.first, pos = t.second;//选择最小边
if(st[pos]) continue;
st[pos] = true;
cnt++;
ans += distance;
//cout << pos << " " << ans << endl;
for(int j = 1; j <= n; j++)
{
if(g[pos][j] < dist[j])
{
//cout << dist[j] << " " << j << endl;
dist[j] = g[pos][j];
q.push({dist[j], j});
}
}
}
if(cnt == n) return ans;
return INF;
}
int main()
{
scanf("%d %d", &n, &m);
memset(g, 0x3f, sizeof g);
while(m--)
{
scanf("%d %d %d", &u, &v, &w);
g[u][v] = min(g[u][v], w);
g[v][u] = min(g[v][u], w);
}
int t = prim();
if(t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}
算法思路:
之前各条边都分别在各自的集合里面,从小到大将其连通起来——并查集
#include
#include
using namespace std;
const int N = 200010;
const int INF = 0x3f3f3f3f;
int n, m, a, b, w;
int f[N];
struct edge
{
int a, b, w;
bool operator<(const edge& e)
{
return w < e.w;
}
}e[N];
int find(int a)
{
if(f[a] != a)
f[a] = find(f[a]);//?
return f[a];
}
int kruskal()
{
sort(e, e + m);//默认less,重载operator<
int cnt = 0, ans = 0;
for(int i = 0; i < m; i++)
{
a = e[i].a, b = e[i].b, w = e[i].w;
int fa = find(a), fb = find(b);
//cout << fa << " " << fb << endl;
if(fa != fb)
{
cnt++;
ans += w;
f[fa] = fb;//union ?
}
}
//n - 1条边
if(cnt == n - 1) return ans;
return INF;
}
int main()
{
scanf("%d %d", &n, &m);
for(int i = 1; i < n; i++)
f[i] = i;
for(int i = 0; i < m; i++)
scanf("%d %d %d", &e[i].a, &e[i].b, &e[i].w);
int t = kruskal();
if(t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}