贪心法
- 基本思想:从问题的某⼀个初始解出发,在每⼀个阶段都根据贪⼼策略来做出当前最优的决策,逐步逼近给定的⽬标,尽可能快地求得更好的解。当达到算法中的某⼀步不能再继续前进时,算法终⽌。
- 基本要素
- 最优⼦结构性质
- 当⼀个问题的最优解⼀定包含其⼦问题的最优解时
- 采⽤反证法证明
- 贪⼼选择性质
- 所求问题的整体最优解可以通过⼀系列局部最优的选择获得,即通过⼀系列的逐步局部最优选择使得最终的选择⽅案是全局最优的
- 最优⼦结构性质
- 得出的结论
- 每个阶段⾯临选择时, 贪⼼法都做出对眼前来讲是最有利的选择
- 选择⼀旦做出不可更改,即不允许回溯
- 根据贪⼼策略来逐步构造问题的解
Dijkstra算法
- 思想:按各个顶点与源点之间路径⻓度的递增次序,⽣成源点到各个顶点的最短路径的⽅法,即先求出⻓度最短的⼀条路径,再参照它求出⻓度次短的⼀条路径,依此类推,直到从源点到其它各个顶点的最短路径全部求出为⽌。
- 算法设计:
- s : 源点。集合S中的顶点到源点的最短路径的⻓度已经确定,集合V-S中所包含的顶点到源点的最短路径的⻓度待定。
- 特殊路径:从源点出发只经过S中的点到达V-S中的点的路径。
- 贪⼼策略:选择特殊路径⻓度最短的路径,将其相连的V-S中的顶点加⼊到集合S中。
- 伪代码表示
dist[s]=0;path[s]=s; 定义存放edge类型的元素的优先队列变量pq; 将edge(from, to, weight) 加入到pq中; while(pq非空) { 队首元素出队,其顶点编号记为(e.to); 检测(e.to)是否第一次出队,如果不是,直接执行下一次循环,如果是则进行如下操作; 更新边权; 记录(e.to)结点是由哪个点指向的; 依次找到与队首元素邻接的各顶点v,计算源点到v的距离,如果新的距离比dist[v]小,则将dist[v]赋值为新的距离,并将edge(e.to, v[e.to][i].to, v[e.to][i].weight + dist[e.to])加入pq中。 }
- 这里特别注意,用到了
v[e.to][i].weight + dist[e.to]
,由当前点到源点的最短距离,加上边权,而不是单纯的边权入队。 - 这里的优先队列就是贪心的思想,每次取的都是目前在队列中到源点最小的距离weight,从(from → \rightarrow →to)的边
这里有一个例题,见下图:
求解过程的表格:
dist数组变化过程
S | V-S | dist[1] | dist[2] | dist[3] | dist[4] | dist[5] | |
---|---|---|---|---|---|---|---|
初始 | {0} | {1,2,3,4} | 8 | -1 | -1 | -1 | -1 |
1 | {0,1} | {2,3,4} | 8 | -1 | 23 | -1 | -1 |
2 | {0, 1, 3} | {2,4} | 8 | 24 | 23 | -1 | -1 |
3 | {0, 1, 3} | {2,4} | 8 | 24 | 23 | 30 | -1 |
p数组变化过程:
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
初始 | 0 | -1 | -1 | -1 | -1 | -1 |
1 | 0 | 0 | -1 | -1 | -1 | -1 |
2 | 0 | 0 | -1 | 1 | -1 | -1 |
3 | 0 | 0 | 1 | 1 | -1 | -1 |
4 | 0 | 0 | 1 | 1 | 3 | -1 |
详细完整代码如下
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
#define MAX 1005
struct edge {
// 从哪个结点开始
int from;
// 指向哪个结点
int to;
// 边权
int weight;
// 默认构造
edge(int from, int to, int weight) {
this->from = from;
this->to = to;
this->weight = weight;
}
};
// 为了使用优先队列,需要对 '<' 进行重载
bool operator<(const edge &e1, const edge &e2) {
// 从小到大排列
return e1.weight > e2.weight;
}
// 用邻接链表的方式对图进行存储
vector<edge> v[MAX];
// 存储下标index的点到源点的最短距离
int dist[MAX];
// 记录路径
int p[MAX];
// 源点
int s = 0;
// 点的个数
int num_point = 0;
// 预处理,dist数组中的元素在未被访问过之前全部标注为-1,含义是无穷大
void pre_solve() {
for (int i = 0; i < num_point; i++) {
dist[i] = -1;
p[i] = -1;
}
}
// 打印路径
void path(int i) {
// 递归回溯答应
if (i == s) {
// 回溯到源点为止
printf("%d\n", s);
return;
}
if (i == -1) {
printf("此路不通\n");
return ;
}
printf("%d<-", i);
path(p[i]);
}
int main() {
int n = 0;
priority_queue<edge> pq;
printf("请输入点的数量 : ");
scanf("%d", &num_point );
// 进行预处理
pre_solve();
printf("请输入边的条数 : ");
scanf("%d", &n );
int from, to, weight;
for (int i = 0; i < n; i++) {
// 读入数据,建图
scanf("%d%d%d", &from, &to, &weight);
v[from].push_back(edge(from, to, weight));
}
printf("请输入源点 : ");
scanf("%d", &s);
// 源点到源点的距离是0
dist[s] = 0;
// 源点的上一个没有,这里就赋值为他自己,便于回溯
p[s] = s;
/**
* 将源点相连的边全部加入优先队列中
* 同时,将p(路径数组)的上一个全部置为源点
*/
int len = v[s].size();
for (int i = 0; i < len; i++) {
pq.push(v[s][i]);
p[v[s][i].to] = s;
}
// 当队列空的时候,表明所有的边都已经访问过,才退出
while (pq.empty() == false ) {
edge e = pq.top();
pq.pop();
/**
* 如果dist数组中,index = e.to位置的元素已经访问过了,就不需要再访问一遍了,
* 因为,源点到index点的最短距离,如果有,那么只会出现在第一次出队的时候,
* 后面出队的,一定没有前面出队的距离短————优先队列的效果
*/
if (dist[e.to] != -1) {
continue;
}
// 此时是第一次出队,边权赋值
dist[e.to] = e.weight;
// 记录路径
p[e.to] = e.from;
len = v[e.to].size();
// 将与其相连的所有边全部放到优先队列中
for (int i = 0; i < len; i++) {
pq.push(edge(e.to, v[e.to][i].to, v[e.to][i].weight + dist[e.to]));
}
}
// 打印相应结点的路径和距离信息
for (int i = 0; i < num_point; i++) {
if (dist[i] == -1) {
printf("点%d无法从源点到达,距离无穷大\n", i);
continue;
}
printf("到源点的距离为 : %d ", dist[i]);
path(i);
}
return 0;
}
运行结果: