贪心算法的个人浅薄理解

贪心算法

说在前面,转载的很多好文章都有利于理解贪心,涉及到侵权会删除的!

一、北鼻先来搞清楚概念哇!

较为正式的解释都是这样的:
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

链接:https://leetcode-cn.com/tag/greedy/
来源:力扣(LeetCode)

可以看看这篇,哈哈我觉得讲的好好玩也容易懂
小白带你学贪心算法【作者:脚本之家】
看完了之后大概知道贪心算法是什么了吧!
把整体最优解的问题转化为局部最优解哟
哎呀我也知道,那难点就是在,怎么找局部最优解捏,继续看吧!

二、判断啥时候要用到贪心捏?

首先都说贪心的两大特性是:最优子结构性质和贪心选择性质。
我自己的理解是看到题目:

  1. 要求最优解(最大最少最多最少最长最短等等)!
  2. .这个要求的最优解是在某个约束条件下的(比如背包问题的这个包只能装50公斤,纸币的面值和要求的金额,路径的长度等等)。
  3. 最优解可以化解为局部最优解去求,没错就是这个局部最优解才是重点。

也可以看看这个贪心跟动态规划的区别,我觉得讲得很好。
贪心算法和动态规划的个人理解 作者:(csdn)谁也不知道会怎样
这个是讲动态规划的,先马克!
从零开始学动态规划 作者:houjingyi233

三、来看看贪心的一些简单例子吧!

我自己理解是,做题的时候是看四点:

  1. 这个要求的最优解是啥?
  2. 约束条件是啥?
  3. 可以分解为怎么样的子结构?
  4. 怎么样才能有这个子结构的最优解?

可以先看看这个博客的前3个经典的例子,比较好理解哟
五大常用算法之三:贪心算法 作者:hust_chen

这里拿第一个举下栗子哈~
贪心算法的个人浅薄理解_第1张图片
看完那个博客对贪心的例子有一定的了解了吧~
注意前三个栗子都是求解之前已经排好序的哟,这也是找最优解的的选择策略。
那现在就来看看了LeetCode上面的题吧!
从简单到中等哈!
按照顺序,仅仅是我自己做过的哈
122 买卖股票的最佳时机||
看看第二个题解,讲得很好喔
贪心算法的个人浅薄理解_第2张图片
1221 分割平衡字符串
贪心算法的个人浅薄理解_第3张图片
55 跳跃游戏
偷懒了,看官方题解吧嘻嘻嘻
在这里插入图片描述

四、一个很糙的贪心模板

int greedy(int[] a){
 
    // 要求的最优解,那倒不一定是int型啦
    int bestSolution;
    // 大噶发现了没有!!循环是一定要有的!!i开始的下标就要看题目而定了
    for ( int i=0; i<n; i++ ) {
        // 判断x是否满足约束条件
        if( isFeasible(xxxx) ){
            //做累加或者其他选择的操作
        }
    }
    return bestSolution;
    //哎呀我知道很糙很糙,重点就是遍历的时候那个找最优解
    //像之前博客里123题,先排好序再遍历就是一种找最优解的方法哈
}

五、扩展

都说prim和kruskal方法求最小生成树是贪心算法里很好的运用哟,也可以一起看看

//求最小生成树
#include "stdafx.h"
#include 
#include 
#include 
using namespace std;

//prim算法,找跟金钟罩距离最近的新点
struct node
{
	double lowcost;//结点的当前最小权重
	int adj;//跟结点连接权重最小的结点
	bool isin=false;//结点是否被访问过
}Node[100];
//预设的每个结点花费设其无限大
//指结点i和几点j之间(边)的权重/花费
double cost[100][100] = { 1000000 };
//输入的结点的个数
int N;

//输入结点的自己权重和结点之间(边)的权重
//根据具体题目定,因为有些题目是没有节点自己的权重的
void input_prim() {
	int i;
	//有N个节点
	cin >> N;
	//输入结点的自己权重
	for (i = 0; i <= N; i++){
		cin >> cost[0][i];
		/*Node[i].adj = 0;
		Node[i].lowcost = cost[0][i];*/
	}
	//输入结点之间(边)的权重
	for (i = 1; i <= N; i++)
		for (int j = 1; j <= N; j++)
			cin >> cost[i][j];
}

//求最小生成树
void prim() {
	int i, j;
	//最小值和最小权重
	double min = 100000,sum=0;
	//每次选择最小权重结点的下标
	int index;
	Node[0].isin = true;
	//给每个结点赋权重,把相邻结点先设成0(即还没有),这一步其实可以直接在input那里赋值啦
	for (i = 1; i <= N; i++) {
		Node[i].adj = 0;
		Node[i].lowcost = cost[0][i];
	}
	//遍历每个结点,开始找
	for (int t = 1; t <= N; t++) {
		//把min设为无限大,相当于一个入口,如果两个结点之间有边就能进,每边就不能进
		min = 10000;
		//在i的循环里会每次更新min值,也就是金钟罩外,与金钟罩连接找到最小结点和其权重
		//Node[i].lowcost,与第i个节点的连接的最小权重也会在加入新的结点后不断地更新
		for (i = 1; i <= N; i++) {
			if (!Node[i].isin && Node[i].lowcost < min) {
				index = i;
				min = Node[i].lowcost;
			}
		}
		//累加权重
		sum += min;
		//标记这个节点已经被访问
		Node[index].isin = true;
		//上面i的循环是找新的结点归入金钟罩
		//下面j的循环是求把这个节点归入金钟罩之后,找到加入新的结点后
		//更新加入新结点与连接金钟罩更小的权重,没有更小的就保持之前金钟罩内结点与其他结点连接最小的
		for (j = 1; j <= N; j++) {
			if (Node[j].lowcost > cost[index][j]) {
				//更新第j个节点连接的最小权重
				Node[j].lowcost = cost[index][j];
				//更新第j个节点连接的最小权重的下标就是加入的新结点
				Node[j].adj = index;
			}
		}
	}
	cout << "我是prim" << endl;
	cout << sum << endl;
}


//**********************************

//Kruskal算法,挑边,把边从大到小排序,然后每次加入短的边(要求加入这条边不成环)
//也就是说每次加入都要看这条边的两个点是不是已经在同一个金钟罩里,可以用并交集的思想,找这两个点是不是有同一个祖先,如果是就不能加,如果不是才能加
//结构体跟prim的不一样
struct node_k
{
	int from;//边的起点
	int to;//边的终点
	int cost;//边的权重
}edge_k[100];
//比prim多了一个数组Tree,用来记录每个结点的父节点,从而找到根节点
int Tree[100];
//另外要注意对于边集edge_k来说,它的个数是结点个数+边数,所以要用一个m来计数表示
int m = 1;
//输入的结点的个数,前面有这里就不写了
//int N;

void input_k() {
	int i,j;
	cin >> N;
	//为了方便多一个n[]和一个p[][]来存放输入结点的权重和边的权重
	int n[100],p[100][100];
	//输入结点自己的权重,但要注意把这个节点权转化,放入边集
	//每个结点都来自结点0(不存在),to自己本身,cost就是自己的权重
	for (i = 1; i <= N; i++) {
		cin >> n[i];
		edge_k[m].from = 0;
		edge_k[m].to = i;
		edge_k[m].cost = n[i];
		m++;
	}
	//输入邻接矩阵,因为是无向图的话,只需要记录上三角/下三角的边,所以只当ij重复的
	for (i = 1; i <= N; i++) {
		for (j = 1; j <= N; j++) {
			cin >> p[i][j];
			if (i < j) {
				edge_k[m].from = i;
				edge_k[m].to = j;
				edge_k[m].cost = p[i][j];
				m++;
			}
		}
	}
	//边集个数=结点数+边数-1,m最后还加了一次,所以要减1
	m--;
}

//机构体的排序重写函数,注意这里是写node_k而不是edge_k
bool cmp(node_k a, node_k b){
	return a.cost < b.cost;
}

//找根节点啦,找自己的大佬啦,有两个方法,记得哪个用哪个
int findroot(int x) {
	//方法一 递归找
	if (Tree[x] == -1)
		return x;
	else {
		return findroot(Tree[x]);
		//这样写就是多加了路径压缩
		//int tmp = findroot(Tree[x]); //递归查找
		//Tree[x] = tmp;           //路径压缩
		//return tmp;

	}
}
//	方法二 不递归
int findroot2(int root) {
	int son, tmp;
	son = root;
	while (root != Tree[root]) //寻找根结点
		root = Tree[root];
	while (son != root) //路径压缩
	{
		tmp = Tree[son];
		Tree[son] = root;
		son = tmp;
	}
	return root;
}

void kruskal() {
	//a,b是判断两个结点的根节点是否相同,是不是一个门派啦~
	//cnt是用来判断,当加入的边数等于点数-1时是最小生成树,可以跳出循环
	int i,a,b,cnt = 0;
	double sum = 0;
	//把每个结点的根节点都指向-1,代表开始时每个结点都是独立的
	for (i = 0; i <= N; i++) Tree[i] = -1;
	//先对边集edge_k,按照边的大小进行从小到大排序,这样一开始选也会先选权重最小的点,后面再选边小的
	sort(edge_k, edge_k + m, cmp);
	//开始选边!
	for (i = 1; i <= N; i++) {
		a = findroot(edge_k[i].from);
		b = findroot(edge_k[i].to);
		//如果这条边的两个结点不是一个门派的,就可以加入
		if (a != b) {
			//a加入门派(金钟罩),父节点接到b就好了,这样反正都是同一个根节点
			//也就是连通分支合并
			Tree[a] = b;
			sum += edge_k[i].cost;
			cnt++;
		}
		//边数=结点数-1时跳出,cnt本来就多加了1,所以刚好相等的时候实际就是cnt=N-1
		if (cnt == N) break;
	}
	cout << "我是kruskal" << endl;
	cout << sum << endl;
}

int main() {
	input_prim();
	prim();
	input_k();
	kruskal();
	return;
}

有哪里不对的请指点,我会改正的!!

你可能感兴趣的:(贪心算法的个人浅薄理解)