这是我参与「第五届青训营 」笔记创作活动的第17天
例题1:会场安排问题
会场安排问题来源于实际,事实上无论任何与时间分配有关的问题都要考虑安排问题,来达到占用公共资源最少且花费时间最短的要求。
1.问题描述
设有n个会议的集合C={1,2,....,n},其中每个会议都要求使用同一个资源(如会议室),而在同一个时间只能有一个会议使用该资源。每一个会议i都提供要求使用该资源的起始时间bi和结束时间ei,且bi<ei。如果选择了会议i使用会议室,则它在半开区间[bi,ei)内占用资源。如果[bi,ei)和[bj,ej)不想交,则称会议i与会议j是相容的。
2.贪心策略
每次从剩下未安排的会议中选择具有最早结束时间且不会与已安排的会议重叠的会议来安排。这样可以使下一个会议尽早开始。
该算法的贪心选择的意思是使剩余的可安排时间段极大化,以便安排尽可能多的相容会议。
3.算法的设计与描述
GreedySelector算法设计思路:
(1)初始化。将n个会议的开始时间储存在数组B中;将n个会议的结束时间储存在数组E中且按照结束时间的非减序排列。采用数组A来存储问题的解。会议i如果在集合A中,当且仅当A[i]=true;
(2)根据贪心策略,算法GreedySelector首先选择会议1,即令A[1]=true.
(3)依次扫描每一个会议
GreedySelector算法描述
void GreedySelector(int n,struct time B[],struct time E[],bool A[])
{
E中元素按非减序排列,B中对应元素做相应调整;
int i,j;
A[1]=true; //初始化选择会议的集合A,即现在只包含会议1
j = 1,i = 2; //从会议 i 开始寻找与会议 j 相容的会议
while( i <= n ){
if(B[i]>=E[j]){
A[i] = true;
j = i;
}
else{
A[i] = false;
}
}
}
4.算法分析
从上诉算法分析中可以得知,该算法的时间主要消耗在各个活动按结束时间从小到大排列。若采用快速排序算法进行排列,算法的时间复杂度为O(nlogn)。显然该算法的空间复杂性是常数阶,即S(n)=O(1).
例题2:单源最短路径问题
1.问题描述
给定一个有向带权图G=(V,E),其中每条边的权是一个非负实数。另外,给定V中的一个顶点,称为源点。现在要计算从源点到所有其他各个顶点的最短路径长度。这里的路径是指路径上经过的所有边上的权值之和。这个问题通常被称为单源最短路径问题。
2.Dijkstra算法思想及算法设计
(1)算法思想
迪杰斯特拉提出按各个顶点与源点之间路径长度的递增次序,生成源点到各个顶点的最短路径的方法,即先求出长度最短的一条路径,再参照它求出长度次短的一条路径。以此类推,直到从源点到其他各个顶点的最短路径全部求出为止。
(2)算法设计
假定源点为u,顶点集合V被划分为两部分:集合S和集合V-S,其中S中的顶点到源点的最短路径的长度已经确定,集合V-S中所包含的顶点到源点的最短路径的长度待定,称从源点出发只经过S中的点到达V-S中的点的路径为特殊路径。
3.单源最短路径问题的构造实例
在如图所示的有向带权图中,求源点0到其余顶点的最短路径及最短路径长度。
4.算法描述
n:顶点个数;
u:源点;
C[n][n]:带权领接矩阵;
dist[ ]:记录某顶点与源点u的最短路径长度;
p[ ]:记录某顶点到源点的最短路径上的该顶点的前驱顶点。
#include <stdio.h>
#define V 20 //顶点的最大个数
#define INFINITY 65535
typedef struct {
int vexs[V]; //存储图中顶点数据
int arcs[V][V]; //二维数组,记录顶点之间的关系
int vexnum, arcnum; //记录图的顶点数和弧(边)数
}MGraph;
//根据顶点本身数据,判断出顶点在二维数组中的位置
int LocateVex(MGraph * G, int v) {
int i = 0;
//遍历一维数组,找到变量v
for (; i < G->vexnum; i++) {
if (G->vexs[i] == v) {
break;
}
}
//如果找不到,输出提示语句,返回-1
if (i > G->vexnum) {
printf("no such vertex.\n");
return -1;
}
return i;
}
//构造无向有权图
void CreateDG(MGraph *G) {
printf("输入图的顶点数和边数:");
scanf("%d %d", &(G->vexnum), &(G->arcnum));
printf("输入各个顶点:");
for (int i = 0; i < G->vexnum; i++) {
scanf("%d", &(G->vexs[i]));
}
for (int i = 0; i < G->vexnum; i++) {
for (int j = 0; j < G->vexnum; j++) {
G->arcs[i][j] = INFINITY;
}
}
printf("输入各个边的数据:\n");
for (int i = 0; i < G->arcnum; i++) {
int v1, v2, w;
scanf("%d %d %d", &v1, &v2, &w);
int n = LocateVex(G, v1);
int m = LocateVex(G, v2);
if (m == -1 || n == -1) {
return;
}
G->arcs[n][m] = w;
G->arcs[m][n] = w;
}
}
//迪杰斯特拉算法,v0表示有向网中起始点所在数组中的下标
void Dijkstra_minTree(MGraph G, int v0, int p[V], int D[V]) {
int final[V];//为各个顶点配置一个标记值,用于确认该顶点是否已经找到最短路径
//对各数组进行初始化
for (int v = 0; v < G.vexnum; v++) {
final[v] = 0;
D[v] = G.arcs[v0][v];
p[v] = 0;
}
//由于以v0位下标的顶点为起始点,所以不用再判断
D[v0] = 0;
final[v0] = 1;
int k = 0;
for (int i = 0; i < G.vexnum; i++) {
int min = INFINITY;
//选择到各顶点权值最小的顶点,即为本次能确定最短路径的顶点
for (int w = 0; w < G.vexnum; w++) {
if (!final[w]) {
if (D[w] < min) {
k = w;
min = D[w];
}
}
}
//设置该顶点的标志位为1,避免下次重复判断
final[k] = 1;
//对v0到各顶点的权值进行更新
for (int w = 0; w < G.vexnum; w++) {
if (!final[w] && (min + G.arcs[k][w] < D[w])) {
D[w] = min + G.arcs[k][w];
p[w] = k;//记录各个最短路径上存在的顶点
}
}
}
}
int main() {
MGraph G;
CreateDG(&G);
int P[V] = { 0 }; // 记录顶点 0 到各个顶点的最短的路径
int D[V] = { 0 }; // 记录顶点 0 到各个顶点的总权值
Dijkstra_minTree(G, 0, P, D);
printf("最短路径为:\n");
for (int i = 1; i < G.vexnum; i++) {
printf("%d - %d的最短路径中的顶点有:", i, 0);
printf(" %d-", i);
int j = i;
//由于每一段最短路径上都记录着经过的顶点,所以采用嵌套的方式输出即可得到各个最短路径上的所有顶点
while (P[j] != 0) {
printf("%d-", P[j]);
j = P[j];
}
printf("0\n");
}
printf("源点到各顶点的最短路径长度为:\n");
for (int i = 1; i < G.vexnum; i++) {
printf("%d - %d : %d \n", G.vexs[0], G.vexs[i], D[i]);
}
return 0;
}
例题3:背包问题
贪心策略:单位重量价值大的优先
代码实现
#include <stdio.h>
#define N 3 //设定商品数量
//根据收益率,对记录的商品进行从大到小排序
void Sort(float w[], float p[]) {
int i,j;
float temp;
float v[N] = { 0 };
//用v[]存商品的收益率
for (i = 0; i < N; i++)
v[i] = p[i] / w[i];
//根据 v 数组记录的各个商品收益率的大小,同时对 w 和 p 数组进行排序
for (i = 0; i < N; i++) {
for (j = i + 1; j < N; j++) {
if (v[i] < v[j]) {
temp = v[i];
v[i] = v[j];
v[j] = temp;
temp = w[i];
w[i] = w[j];
w[j] = temp;
temp = p[i];
p[i] = p[j];
p[j] = temp;
}
}
}
}
/*贪心算法解决部分背包问题
w:记录各个商品的总重量
p:记录各个商品的总价值
result:记录各个商品装入背包的比例
W:背包的容量
*/
void fractional_knapsack(float w[], float p[], float result[], float W) {
float temp = 0;
int i = 0;
//根据收益率,重新商品进行排序
Sort(w, p);
//从收益率最高的商品开始装入背包,直至背包装满为止
while (W > 0) {
temp = W > w[i] ? w[i] : W;
result[i] = temp / w[i];
W -= temp;
i++;
}
}
int main() {
int i;
//统计背包中商品的总收益
float values = 0;
//各个商品的重量
float w[N] = { 10,30,20 };
//各个商品的收益
float p[N] = { 60,100,120 };
float result[N] = { 0 };
//调用解决部分背包问题的函数
fractional_knapsack(w, p, result, 50);
//根据 result 数组中记录的数据,决定装入哪些商品
for (i = 0; i < N; i++) {
if (result[i] == 1) {
printf("总重量为 %f,总价值为 %f 的商品全部装入\n", w[i], p[i]);
values += p[i];
}
else if (result[i] == 0)
printf("总重量为 %f,总价值为 %f 的商品不装\n", w[i], p[i]);
else {
printf("总重量为 %f,总价值为 %f 的商品装入 %f%%\n", w[i], p[i], result[i] * 100);
values += p[i] * result[i];
}
}
printf("最终收获的商品价值为 %.2f\n", values);
return 0;
}