利用贪心算法解决最小平铺路径问题
一、题目描述
设X是实数轴上n个区间的集合,X={[l1, u1], [l2, u2], …, [l1, un]}。假设集合X中的所有区间集合覆盖实数轴上连续的区域。Y是X的子集,如果Y中的区间覆盖X中的所有区间,即X中任意区间内的实数均属于Y的某个区间内,则称Y为X平铺路径。平铺路径的大小是指Y中的区间个数。试设计一个贪心算法求解集合X中最小的平铺路径。
二、分析并解决问题
1、什么是平铺路径
由题目可知,X中区间的并集会覆盖数轴上的一段连续的区域,假设X={[1, 3], [2, 5], [3, 4], [4, 7]},此时,X覆盖了数轴上[1, 7]的区域。我们需要找一个X的子集Y,使Y中的各个区间的交集也覆盖[1, 7],并使Y中的区间数量最少,此时,Y便是最小平铺路径。在这里,Y={[1, 3], [2, 5], [4, 7]}。不要[3, 4]是因为[3, 4]在[2, 5]中,不是必要的区间。
2、寻求解决问题的方法
首先,通读题目我们可以得知,这道题目要求X中最小的平铺路径。这时,我们可以尝试使用贪心算法来解决这个问题。贪心算法一般用来解决求最大或最小解的问题。
3、简单了解贪心算法
贪心算法是指,在解决问题的过程中,总是选择当前看起来最好的选择。也就是说,贪心算法并不是在问题的整体考虑最优解,而是在问题的局部考虑最优解。因此,贪心算法不能保证解是最佳的,因为贪心算法没有“全局观”。
但并不是说贪心算法不能获取一个问题的最优解,它能对很多问题求出整体最优解,比如最小生成树问题。
想要获取整体最优解,要选择合适的贪心策略,并证明其优化子结构和贪心选择性。
3.1 贪心策略
贪心策略是指做出当前看起来最优的选择的策略。比如找钱的问题,想要找出硬币个数最少的找零搭配,很容易选出一个贪心策略,就是尽可能选择面额大的硬币。不难想到,硬币的面额越大,总的数量就越少。
当然,这种贪心策略不能得出全局最优解,只能获取较优解。比如,需要找8块钱,现在有5块、4块和1块三种面额的硬币,此时,我们如果使用上面说的贪心策略,会得到这样的过程:
先找尽可能大面额的硬币,就是5块的,此时还剩3块;然后继续找面额较大的硬币,就是1块的,这时拿3个一块钱的硬币。总的需要1个5块和3个1块的硬币,总共需要4个硬币。但很明显,我们拿2个4块的硬币就可以了。所以这种贪心策略不能得到最优解,但是可以得到较优解(总比拿8个一块的好)。
需要注意的是,一个问题可能有多种贪心策略。
3.2 优化子结构
优化子结构虽然很抽象,但还是比较好理解的,动态规划之类的算法也有这个概念,指的是一个问题的最优解包含其子问题的最优解。这里就不展开讲解了。
3.3 贪心选择性
贪心选择性指的是一个问题的整体最优解可通过一系列局部的最优解的选择来得到,并且每次的选择可以依赖以前作出的选择,但不能依赖于后面要作出的选择(不具有后瞻性)。
就像上面找钱问题的贪心策略,最后不能得到全局最优解,所以就不具有贪心选择性。
一般证明贪心选择性可以使用归纳法。
4、解决当前问题
4.1 选择贪心策略
为了获取最小平铺路径,每次选li最小而(ui-li)最大的区间,使我们能够选择最少的区间使其能覆盖X中所有的区间。简单来说就是,从最左边开始,尽可能选择长的区间,直到整个X覆盖的区域被填满。
(由于这里不好打数学符号,之后的证明将会用截图的方式展现)
此时,每做一次操作,剩余子问题是:
4.2 证明贪心选择性
4.3 证明优化子结构
三、算法实现
1、c语言实现
定义区间结构体,x是左界,y是右界:(node.h)
#ifndef __NODE_H__
#define __NODE_H__
// node
typedef struct Node{
int x;
int y;
} Node;
#endif
定义布尔类型枚举:(bool.h)
#ifndef __BOOL_H__
#define __BOOL_H__
// bool type
typedef enum {
false, true
} bool;
#endif
实现不具有稳定性的快速排序(这个不是刚需,只是实现排序功能):(quicksort.c)
#include "bool.h"
#include "node.h"
// 快速排序(不稳定)
// x -> true : 对前界排序
// asc -> true : 升序排序
void quickSort(Node* nodes, int size, bool x, bool asc){
_quickSort(nodes, 0, size - 1, x, asc);
}
// 快速排序
void _quickSort(Node* nodes, int left, int right, bool x, bool asc){
if(left < right){
int q = partition(nodes, left, right, x, asc);
_quickSort(nodes, left, q - 1, x, asc);
_quickSort(nodes, q + 1, right, x, asc);
}
}
// 分区
int partition(Node* nodes, int left, int right, bool x, bool asc){
Node std = *(nodes + right);
int i = left - 1;
for(int j = left; j < right; j++){
if(asc && x && (nodes + j)->x <= std.x){
i = i + 1;
swap(nodes, i, j);
} else if(asc && !x && (nodes + j)->y <= std.y){
i = i + 1;
swap(nodes, i, j);
} else if(!asc && x && (nodes + j)->x >= std.x){
i = i + 1;
swap(nodes, i, j);
} else if(!asc && !x && (nodes + j)->y >= std.y){
i = i + 1;
swap(nodes, i, j);
}
}
swap(nodes, i + 1, right);
return i + 1;
}
// 交换元素
void swap(Node* nodes, int i, int j){
Node temp = *(nodes + i);
*(nodes + i) = *(nodes + j);
*(nodes + j) = temp;
}
实现具有稳定性的归并排序(这个不是刚需,只是实现排序功能,但需要稳定性)
#include "bool.h"
#include "node.h"
// 归并排序(稳定的)
// x -> true : 对前界排序
// asc -> true : 升序排序
void mergeSort(Node* nodes, int size, bool x, bool asc){
_mergeSort(nodes, 0, size - 1, x, asc);
}
// 归并排序
void _mergeSort(Node* nodes, int left, int right, bool x, bool asc){
if(left < right){
int mid = (left + right) / 2;
_mergeSort(nodes, left, mid, x, asc);
_mergeSort(nodes, mid + 1, right, x, asc);
_merge(nodes, left, mid, right, x, asc);
}
}
// 归并
void _merge(Node* nodes, int left, int mid, int right, bool x, bool asc){
// 暂存结果
Node temp[right - left + 1];
// 左右索引
int l_idx = left;
int r_idx = mid + 1;
int temp_idx = 0;
// 归并
while(l_idx <= mid && r_idx <= right){
if(x && asc){
if((nodes + l_idx)->x <= (nodes + r_idx)->x){ // 这个比较的等于号是稳定性的关键
temp[temp_idx++] = *(nodes + l_idx);
l_idx++;
} else {
temp[temp_idx++] = *(nodes + r_idx);
r_idx++;
}
} else if(x && !asc){
if((nodes + l_idx)->x >= (nodes + r_idx)->x){
temp[temp_idx++] = *(nodes + l_idx);
l_idx++;
} else {
temp[temp_idx++] = *(nodes + r_idx);
r_idx++;
}
} else if(!x && asc){
if((nodes + l_idx)->y <= (nodes + r_idx)->y){
temp[temp_idx++] = *(nodes + l_idx);
l_idx++;
} else {
temp[temp_idx++] = *(nodes + r_idx);
r_idx++;
}
} else if(!x && !asc){
if((nodes + l_idx)->y >= (nodes + r_idx)->y){
temp[temp_idx++] = *(nodes + l_idx);
l_idx++;
} else {
temp[temp_idx++] = *(nodes + r_idx);
r_idx++;
}
}
}
// 将剩余的部分附在最后
while(l_idx <= mid){
temp[temp_idx++] = *(nodes + l_idx);
l_idx++;
}
while(r_idx <= right){
temp[temp_idx++] = *(nodes + r_idx);
r_idx++;
}
// 将暂存的数据写回
for(int i = 0; i < right - left + 1; i++){
*(nodes + left + i) = temp[i];
}
}
测试代码:(main.c)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "bool.h"
#include "node.h"
#define SIZE 10 // S 中元素的个数
#define random(min, max) (((min) + rand()%((max)-(min)))+1) // 设置随机数范围
#define RANGE_MIN 0 // 数轴最小值
#define RANGE_MAX 20 // 数轴最大值
// 输入变量
Node S[SIZE];
// 结果集合
Node result[SIZE];
// 结果集合尾部下标
int top = 0;
int main()
{
// 先对y进行降序排序,再对x进行稳定的升序排序是为了构造,x从小到大,当x相同时,y从大到小的区间集合
// 生成随机段
randomArr();
printf("随机数集:\n");
printArr(S);
// 先对 y 排序
quickSort(S, SIZE, false, false);
//printf("后界排序后:\n");
//printArr(S);
// 再对 x 排序
mergeSort(S, SIZE, true, true);
//printf("前界排序后:\n");
//printArr(S);
// 构造结果集
getResults();
// 打印结果集
printResult();
return 0;
}
// 构造结果集(实现贪心策略)
void getResults(){
// 先把第一个元素加到结果集中
result[top++] = S[0];
// 开始遍历
for(int i = 1; i < SIZE; i++){
if((S[i].x <= result[top-1].y) && (S[i].y > result[top-1].y)){
result[top++] = S[i];
}
}
}
// 生成随机段
void randomArr(){
srand((unsigned) time(NULL));
for(int i = 0; i < SIZE; i++){
Node node;
node.x = random(RANGE_MIN, RANGE_MAX);
node.y = random(RANGE_MIN, RANGE_MAX);
// 使 y 大于 x
while(node.x >= node.y){
if(node.x == RANGE_MAX){
break;
}
if(node.x == RANGE_MAX - 1){
node.y = RANGE_MAX;
} else {
node.y = node.y + random(node.x - node.y + 1, RANGE_MAX - node.y);
}
}
S[i] = node;
}
}
// 打印数组
void printArr(Node* nodes){
for(int i = 0; i < SIZE; i++){
printf("no.%d->x:%d,y:%d\n", i, (nodes + i)->x, (nodes + i)->y);
}
}
// 打印结果数组
void printResult(){
printf("结果集合:\n");
for(int i = 0; i < top; i++){
printf("no.%d->x:%d,y:%d\n", i, (result + i)->x, (result + i)->y);
}
}
四、后记
由于时间问题,这次求解过程介绍地比较草率,其实关键点还是证明贪心算法具有优化子结构和贪心选择性,但实际上,这个证明不是常用,所以大家只要理解什么是贪心算法就够了。实际上,大家平时写的不知名的算法,可能就会用到贪心算法的思想,只是大家不知道罢了。
这次的博客就结束了,代码我会上传资源到我的csdn空间,希望大家有所收获,如果有问题的话可以评论,谢谢观看!