总在做出当前看起来最好的选择,一些情况下即使不能得到整体最优解,但最终结果是最优解的很好的近似解
贪心选择的基本要素
-
贪心选择性质:所求问题的整体最优解可以通过一系列局部最优的选择来达到
-
最优子结构性质:一个问题的最优解包含其子问题的最优解
背包问题
与0-1背包问题类似,所不同的是,在选择物品i装入背包的时候,可以选择物品i的一部分装入背包,而不一定全部装入背包,这是与0-1背包问题的差别
算法思路
把物品按照单位价值进行非降序排列,从单位价值最高的物品开发放,能放则全部放入,否则放入他能放入的部分
代码实现
- 使用物品类代表每个物品
static class Produce{
float weight;//重量
float value;//价值
float wv;//单位价值 v/w
float x;//放多少
int index;//编号
Produce(float weight, float value, int index){
this.weight = weight;
this.value = value;
this.index = index;
this.wv = value/weight;
}
}
- 使用物品数组存储物品
static Produce[] produces;
- 对物品数组按照物品的单位价值非降序
//非降序排列
public static void sort(Produce[] produces){
for(int i = 0; i<produces.length-1; i++){
for(int j = 0; j<produces.length-i-1; j++){
if(produces[j].wv>produces[j+1].wv){
Produce temp = produces[j];
produces[j] = produces[j+1];
produces[j+1] = temp;
}
}
}
}
- 使用贪心策略求解,对物品i能放入则全放,否则只放入部分
public static void greedy(Produce[] produces, int c, int v){
for(int i = 0; i<produces.length; i++){
if(produces[i].weight <= c){
c-=produces[i].weight;
v+=produces[i].value;
produces[i].x = 1;
}else {
v+=c*produces[i].wv;
produces[i].x = (float)c/produces[i].weight;
break;
}
}
}
- 输出结果
public static void printResult(Produce[] produces){
for(int i = 0; i<produces.length; i++){
if(produces[i].x == 1){
System.out.println("物品"+produces[i].index+"全装");
}else if(produces[i].x == 0){
break;
}else {
System.out.println("物品"+produces[i].index+"装"+produces[i].x);
}
}
}
- 完整代码
import java.util.Scanner;
public class BBProblem {
static class Produce{
float weight;//重量
float value;//价值
float wv;//单位价值 v/w
float x;//放多少
int index;//编号
Produce(float weight, float value, int index){
this.weight = weight;
this.value = value;
this.index = index;
this.wv = value/weight;
}
}
static int n;//物品数
static int c;//背包容量
static int v = 0;//当前价值
static Produce[] produces;
//非降序排列
public static void sort(Produce[] produces){
for(int i = 0; i<produces.length-1; i++){
for(int j = 0; j<produces.length-i-1; j++){
if(produces[j].wv>produces[j+1].wv){
Produce temp = produces[j];
produces[j] = produces[j+1];
produces[j+1] = temp;
}
}
}
}
public static void greedy(Produce[] produces, int c, int v){
for(int i = 0; i<produces.length; i++){
if(produces[i].weight <= c){
c-=produces[i].weight;
v+=produces[i].value;
produces[i].x = 1;
}else {
v+=c*produces[i].wv;
produces[i].x = (float)c/produces[i].weight;
break;
}
}
}
public static void printResult(Produce[] produces){
for(int i = 0; i<produces.length; i++){
if(produces[i].x == 1){
System.out.println("物品"+produces[i].index+"全装");
}else if(produces[i].x == 0){
break;
}else {
System.out.println("物品"+produces[i].index+"装"+produces[i].x);
}
}
}
public static void main(String[] args) {
System.out.println("请输入背包容量");
Scanner scanner = new Scanner(System.in);
c = scanner.nextInt();
System.out.println("请输入物品数");
n = scanner.nextInt();
produces = new Produce[n];
for(int i = 0; i<n; i++){
System.out.println("输入物品重量");
float m = scanner.nextFloat();
System.out.println("请输入物品价值");
float k = scanner.nextFloat();
Produce produce = new Produce(m, k, i);
produces[i] = produce;
}
sort(produces);
greedy(produces,c,v);
printResult(produces);
}
}
活动安排问题
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si <fi。如果选择了活动i,则它在半开时间区间[si, fi)内占用资源。若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的。也就是说,当si≥fj或sj≥fi时,活动i与活动j相容。活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合。
求解思路
贪心的选择结束时间相对早的并且与之前选择不相容的活动,最后选出来的活动集合就是最大的相容活动子集合
代码实现
- 活动类存储活动
static class Activity{
int startTime;//开始时间
int finishTime;//结束时间
boolean get;//是否选择
int index;//活动编号
Activity(int startTime, int finishTime, int index){
this.startTime = startTime;
this.finishTime = finishTime;
this.index = index;
}
}
- 活动数组存储所有活动
Activity[] activities = new Activity[n];
- 对活动按照结束时间非降序排序
//非降序排序
public static void sort(Activity[] activities){
for(int i = 0; i<activities.length-1; i++){
for(int j = 0; j<activities.length-1-i; j++){
if(activities[j].finishTime>activities[j+1].finishTime){
Activity temp = activities[j];
activities[j] =activities[j+1];
activities[j+1] = temp;
}
}
}
}
- 贪心选择,选结束时间最早且能与其他相容的活动
static void greedy(Activity[] activities){
int j = 0;//记录最近一次添加的活动
activities[0].get = true;
for(int i = 1; i<activities.length; i++){
if(activities[i].startTime >= activities[j].finishTime){
activities[i].get = true;
j = i;
}else {
activities[i].get = false;
}
}
}
- 所有代码
import java.util.Scanner;
public class HDAPProblem {
static class Activity{
int startTime;//开始时间
int finishTime;//结束时间
boolean get;//是否选择
int index;//活动编号
Activity(int startTime, int finishTime, int index){
this.startTime = startTime;
this.finishTime = finishTime;
this.index = index;
}
}
//非降序排序
public static void sort(Activity[] activities){
for(int i = 0; i<activities.length-1; i++){
for(int j = 0; j<activities.length-1-i; j++){
if(activities[j].finishTime>activities[j+1].finishTime){
Activity temp = activities[j];
activities[j] =activities[j+1];
activities[j+1] = temp;
}
}
}
}
static void greedy(Activity[] activities){
int j = 0;//记录最近一次添加的活动
activities[0].get = true;
for(int i = 1; i<activities.length; i++){
if(activities[i].startTime >= activities[j].finishTime){
activities[i].get = true;
j = i;
}else {
activities[i].get = false;
}
}
}
public static void printResult(Activity[] activities){
for(int i = 0; i<activities.length; i++){
if(activities[i].get == true){
System.out.println("选择活动"+i);
}
}
}
public static void main(String[] args) {
int n = 0;
System.out.println("输入活动数");
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
Activity[] activities = new Activity[n];
for(int i =0; i<n; i++){
System.out.println("输入开始时间");
int startTime = scanner.nextInt();
System.out.println("输入结束时间");
int finishTime = scanner.nextInt();
Activity activity = new Activity(startTime, finishTime, i);
activities[i] =activity;
}
sort(activities);
greedy(activities);
printResult(activities);
}
}
最优装载
有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。
求解思路
每次都先装重量最小的
代码实现
public static void greedy(int[] box,int w, boolean[] getbox){
//box[i]=j指的是箱子i的重量是j,箱子按照重量非降序排序
//w是船的承载能力
//getbox[i] = true指的是物品i放上
for(int i = 0; i<box.length; i++){
if(box[i]<w){
getbox[i] = true;
w-=box[i];
}else {
getbox[i] = false;
}
}
}
单元最短路径
给定带权有向图G =(V,E),其中每条边的权是非负实数。另外,还给定V中的一个顶点,称为源。现在要计算从源到所有其他各顶点的最短路长度。这里路的长度是指路上各边权之和。这个问题通常称为单源最短路径问题
求解思路
输入带权有向图是G=(V,E),V={1,2,…,n},顶点v是源。c是一个二维数组,c[i][j]表示边(i,j)的权。当(i,j)不属于E时,c[i][j]是一个大数。dist[i]表示当前从源到顶点i的最短特殊路径长度
用S[]来表示顶点,最开始只有源在S里,随着算法的进行,会把已经找到最短路的顶点放到S里
也就是说我们从源开始,先找到达源最近的顶点,放入S中,并把这个点记为u,接下来对于其他顶点i来说,我们就有2中走法,第一种是从源到顶点i,而第二种是经过顶点u,再到顶点i,选择两者之间小的更新dist[i],再选择最小的dist[i]的顶点加入S中,他已经找到最优路;
再接下来把刚刚加入的i作为u,对应其他的顶点k来说,还是两种走法,第一种就是不经过顶点u保持之前自己的走法,第二种是经过顶点u再到顶点k,选择最小的更新dist[i],选择dist[i]最小的k加入S
Dijkstra算法可描述如下,其中输入带权有向图是G=(V,E),V={1,2,…,n},顶点v是源。c是一个二维数组,c[i][j]表示边(i,j)的权。当(i,j)不属于E时,c[i][j]是一个大数。dist[i]表示当前从源到顶点i的最短特殊路径长度。在Dijkstra算法中做贪心选择时,实际上是考虑当S添加u之后,可能出现一条到顶点的新的特殊路,如果这条新特殊路是先经过老的S到达顶点u,然后从u经过一条边直接到达顶点i,则这种路的最短长度是dist[u]+c[u][i]。如果dist[u]+c[u][i]<dist[i],则需要更新dist[i]的值。步骤如下:
(1) 用带权的邻接矩阵c来表示带权有向图,
c[i][j]表示弧<vi,vj>上的权值。设S为已知最短路径的终点的集合,它的初始状态为空集。从源点v经过S到图上其余各点vi的当前最短路径长度的初值为:dist[i]=c[v][i],
vi属于V. (2) 选择vu, 使得dist[u]=Min{dist[i] |
vi属于V-S},vj就是长度最短的最短路径的终点。令S=S U {u}.(3) 修改从v到集合V-S上任一顶点vi的当前最短路径长度:如果 dist[u]+c[u][j]< dist[j] 则修改
dist[j]= dist[u]+c[u][j]. (4) 重复操作(2),(3)共n-1次.
代码实现(注释写的很清楚了)
public static void greedy(int[] dist, int[] prev, int[][] point, int source){
boolean[] s = new boolean[dist.length];//s[i] = false 顶点i还没找到最优路
//source:源头
//point[i][j] i-->j的距离
//dist[i]源头到i的最短路径
//prev[i]i的前一个点
for(int i = 1; i<dist.length; i++){//初始化 dist[i]就是顶点source到顶点i的长度,dist[]从下标1开始表示顶点
//相当于填了表的第一行
s[i] = false;
dist[i] = point[source][i];
if(dist[i] == Integer.MAX_VALUE){//到达不了
prev[i] = 0;
}else {
prev[i] = source;
}
}
for(int i = 1; i<dist.length-1;i++){//需要做n-1次
int u = source;//可以通过谁继续找
//找不在s里,即还没找到最优路,且距离集合最近的点
int temp = Integer.MAX_VALUE;
for(int j = 1; j<dist.length; j++){
if((!s[j])&&(dist[j]<temp)){//不是最优且距离集合最近
u = j;
temp = dist[j];
}
}
s[u] = true;//把顶点u放到s里,已经找到最优
//更新dist
for(int j = 1; j<dist.length; j++){
if((!s[j])&&(point[u][j]<Integer.MAX_VALUE)){
//不在集合里且可达
int newdist = dist[u] + point[u][i];
if(newdist<dist[j]){
dist[j] = newdist;
prev[j] = u;
}
}
}
}
}
最小生成树
设G = (V,E)是无向连通带权图,即一个网络。E中的每一条边(v,w)的权为c[v][w]。如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树上各边权的总和称为生成树的耗费。在G的所有生成树中,耗费最小的生成树称为G的最小生成树
在上图中找一个子图,这个子图任意两点可以连通(可直达或间接到达),这样的子图可以找到很多个,但是子图集合中图上所有权的和最小的就只有一个称为最小生成树!
最小生成树与单源最短路径区别?
好多人学习了单源最短路径与最小生成树就会混他两,其实从概念就可以区分,单源最短路径是有权有向图,而最小生成树是有权无向图,其次单源最短路径要求有一个源点,解决的是该源点到其他店的最短路径,而最小生成树是从整体考虑,所有点之间的连通时权之和最小
Prim算法
思路:选择一个起始点比如1,1到所有点中最小的是3。连接13
1到2456中和3到2456中最小的时3到6所以连接36
1到245和 3到245以及6到245中最小的是6到4,所以连接64
然后1到25 3到25 6到25 4到25中最小的是3到2所以连接32
然后1到5 3到5 6到5 4到5 2到5中最小的是2到5所以连接25
至此所有点连接完成,六个点五条边符合最小生成树的特点
以上就是prim算法找最小生成树的原理
import java.util.Scanner;
public class ZXSCS {
public static void prim(int n,float[][] c){
float[] lowcost=new float[n+1];
int[] closest=new int[n+1];//表示i到其他所有未添加进来的顶点的最短距离的点
boolean[] s=new boolean[n+1];//
//初始化
s[1]=true;
for(int i=2;i<=n;i++){
lowcost[i]=c[1][i];
closest[i]=1;
s[i]=false;
}
for(int i=1;i<n;i++){//循环n-1次
float min=Float.MAX_VALUE;
int j=1;
for(int k=2;k<=n;k++){
if(lowcost[k]!=-1&&(lowcost[k]<min)&&(!s[k])){
min=lowcost[k];
j=k;
}
}
System.out.println(closest[j]+", "+j);
s[j]=true;//将j添加到S中
//逐个更改lowcost[k],如果c[j][k]<lowcost[k]则更改lowcost[k]为最小
for(int k=2;k<=n;k++){
if(!s[k]&&c[j][k]!=-1){
if(c[j][k]<lowcost[k]||lowcost[k]==-1){
lowcost[k]=c[j][k];
closest[k]=j;
}
}
}
}
}
public static void main(String[] args) {
System.out.println("请输入图顶点的个数:");
Scanner sc = new Scanner(System.in);
String line = sc.nextLine();
int n = Integer.parseInt(line);
System.out.println("请输入图的路径长度:");
float[][] c = new float[n+1][n+1];//下标从1开始,以下都是,不连通的用-1表示
for(int i=0;i<n;i++){
line = sc.nextLine();
String[] ds = line.split(",");
for(int j = 0;j<ds.length;j++){
c[i+1][j+1]=Float.parseFloat(ds[j]);
}
}
System.out.println("依次构成树的边为(用两个顶点表示边):");
prim(n,c);
}
}
来源:CSDN
作者:今天又学java了
链接:https://blog.csdn.net/weixin_43907800/article/details/103867673