LeetCode算法题0:贪心 - 分发糖果


前言

      本文记录自己在LeetCode上关于一道算法题的解题过程,这道题花费了不少时间才通过。感觉在自己的这种解题风格道路上走远了!但是代码提交的结果还是比较满意的。 哈哈,过程比较曲折,下面为大家一一道来:


一、题目

      老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
      你需要按照以下要求,帮助老师给这些孩子分发糖果:

      要求1:每个孩子至少分配到 1 个糖果。
      要求2:评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。

      那么这样下来,老师至少需要准备多少颗糖果呢?

示例1:

输入:[1,0,2]
输出:5
解释:你可以分别给这三个孩子分发 2、1、2 颗糖果。

示例2:

输入:[1,2,2]
输出:4
解释:你可以分别给这三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

该题所在网址:https://leetcode-cn.com/problems/candy/

二、思路详解

      瞧一瞧,看一看,大家品一下这个老师有多抠门~

      首先:给每个孩子至少给一个糖果,这个简单。要求 2 稍微难理解一点,参考下面这个示例来理解,概括为:如果一个孩子两侧,只要存在一个比他评分低的孩子,那么他的糖果数就要比这个低评分孩子的糖果要多。

这里给出一个示例:所有孩子的评分:{ 1,0,2,3,4,3,2,2 } 它对应的糖果数:{ 2,1,2,3,4,2,1,1 }。后面会对这个示例进一步分析。

      最开始的思路是这样的:按照平移的思想来设计,即;给起始第一个孩子1颗糖果,依次往后判断,如果比它大则糖果加1,比它小则减1,碰到相等的情况则另做分析。 最后将整体结果往上平移(可能有负数存在),使得最小糖果数为1即可。 但是后来发现不可行,逻辑判断太复杂,很难分析。 还有最重要的一点是,糖果数有时候会有一个突然的下降,比如给出的示例中评分为4和3的孩子,前者给4颗糖果,后者给2颗糖果。

      所以单纯的只从左到右的分析是不充分的,当然可以在这个基础上再改改,比如补充上从右到左进行分析,不过在这儿不聊这个。聊点清新脱俗的~


————————————————      华丽丽的分割线


      就糖果数突然有一个下降的现象,我思考了一下原因。从而得到一种解题思路:

      发现:如果能找到那些极小值点,也就是那些最终只分配 1 颗糖果的孩子(好惨!),因为给这些孩子的糖果数是已经确定的了。以此(这些糖果数)为基础,再对这些孩子的左右分别进行分析,很容易就可以确定他们的左右孩子应得的糖果数,然后依次迭代计算扩展至所有孩子。    将整个评分序列想象成为一个函数,接下来要做的是找到其中的极小值点,给它们分配糖果,再对其余的评分序列继续找当前的极小值点,分配糖果。直至分配结束。    想象一下快速排序的特点,每一次循环会确定一个最终排序结束时元素的位置。而目前的这个思路和它类似:每一次会确定若干个极小值点的糖果数。

      所以具体的算法思路如下:
      如果一个孩子的左右两侧评分都大于等于它,那么它就是一个极小值。
      (1) 找到所有极小值点,将它们对应的糖果数量置为1
      (2) 排除上述已经分配了糖果的孩子,在其余孩子中再次找极小值点,将它们对应的糖果数量置为2.(每一轮的极小值点分配的糖果数要比之前的多1个)
      (3) 不断重复步骤2. 直至给所有的孩子都分配了糖果。

      对上面的示例进行分析:给定孩子的评分数组为 { 1,0,2,3,4,3,2,2 }
      第一轮之后糖果数组为:_ 1 _ _ _ _ 1 1 ,其中第二个数 0 ,倒数第二个数 2 和最后一个数 2 为极小值。

      第二轮在第一轮的基础之上,得到两个孩子的评分区间分别为:{ 1 } 和 { 2,3,4,3} 。对这两个数组找它们的极小值,给这些极小值点的糖果数为2,得到第二轮之后糖果数组为:2 1 2 _ _ 2 1 1 ,其中区间 1 中唯一的一个数 1 是极小值,区间 2 中的第一个数 2 和最后一个数 3 为极小值。

      第三轮在第二轮的基础之上,得到一个孩子的评分区间分别为:{ 3,4} 。对这个数组找极小值,给这些极小值点的糖果数为3,得到第三轮之后糖果数组为:2 1 2 3 _ 2 1 1 ,其中第一个数 3 为最小值。

      第四轮在第三轮的基础之上,得到一个孩子的评分区间分别为:{ 4} 。对这个数组找极小值,给这些极小值点的糖果数为4,得到第四轮之后糖果数组为:2 1 2 3 4 2 1 1 ,这个唯一的数为极小值。

      糖果数:{ 2,1,2,3,4,2,1,1 }即为最终结果。

三、搞点实际点儿的(C++实现)

1.略显粗糙的代码实现

      说明:下面的代码有助于对整体思路的把控,并且我在代码中已经给出了详细的注释。缺点是太繁琐,提交的时候会报超时,我稍后会在此之上做一些优化。
      代码如下:

class Solution {
private:
	int count; //用来计数
	int start; //每一轮循环的起始下标
	int end;//每一轮循环的结束下标
	int assignCandy;//每一轮应该给当前极小值点的糖果数
	int sum; //求和,用来返回最终结果
public:


	int candy(vector<int>& ratings) {
		int N = ratings.size();
		//为几个参数设置初始值
		count = 0;
		sum = 0;
		assignCandy = 1;
		start = 0;
		end = N - 1;
		
		int* flag = new int[N];//用来判断哪些孩子已经被分配了糖果,若分配了就置为1
		int* candys = new int[N];//保存给每一个孩子分配的糖果数目
		memset(flag, 0, sizeof(int)*N);//将flag数组初始时全置为0

		setCandy(ratings, flag, candys);//第一轮分配,找到本轮的极小值点,并进行糖果分配
		//在第一轮分配之后,这里采用循环结构来进行每一轮的糖果分配,count表示当前已经分配了糖果的孩子数目,
		//所以退出条件这里写作为 count<N 
		while (count < N)
		{
			assignCandy++;//每一轮待分配的糖果数量要比上一轮多1个
			
			for (int i = 0; i < N; i++)//遍历一遍数组,以上一轮找到的极小值点为基础,得到本轮应该分配糖果的孩子区间。
			{
				if (flag[i] == 0)
				{
					start = i;
					while (i < N && flag[i] == 0)
						++i;
					end = i - 1;
					//每一次调用setCandy时,要先确定当前区间的start和end。
					setCandy(ratings, flag, candys); 
				}

			}
		}
		for (int i = 0; i < N; i++)
			sum += candys[i]; //最后得到所有的糖果数目
		return sum;

	}

	void setCandy(vector<int>& temp, int flag[], int candys[])
	{
		if (start == end) //如果区间里只有一个数的情况,那它就是本轮的极小值,直接分配糖果并返回
		{
			candys[start] = assignCandy;
			flag[start] = 1;
			count++;
			return;
		}
		int i, prev, after;

		//判断区间的第一个数是否为极小值,只需要和第二个数相比较即可。 按照之前分析的,注意这里需要取等号。
		if (temp[start + 1] >= temp[start])
		{
			candys[start] = assignCandy;
			flag[start] = 1;
			count++;
		}
		//判断区间的最后一个数是否为极小值,只需要和倒数第二个数相比较即可。 按照之前分析的,注意这里需要取等号。
		if (temp[end] <= temp[end - 1])
		{
			candys[end] = assignCandy;
			flag[end] = 1;
			count++;
		}
		
		for (prev = start, i = start + 1, after = start + 2; i <= end - 1; ++i, ++after, ++prev)
		{
			//对区间中间的所有数依次遍历,判断是否为极小值,按照之前分析的,注意这里需要取等号。
			if (temp[i] <= temp[prev] && temp[i] <= temp[after])
			{
				candys[i] = assignCandy;
				flag[i] = 1;
				count++;
			}
		}
	}


};

      需要指出一点,也是之前困扰了我比较久的一个问题。上面这个代码,在while(count<N)循环中,有一行代码我之前是写成这样的:

			...
			while (flag[i] == 0 && i < N)
				++i;
			...

      然后呢,它在VS2017中是可以编译并运行的,但是在LeetCode中会报错:“AddressSanitizer: heap-buffer-overflow on address…” ,个人猜测,在VS中对 && 应该是有做过优化的,而在LeetCode中是严格按照从左到右来判断的,所以会出现数组访问越界的现象。 解决方法:把 && 的左右互换一下就行。先判断 i<N 就好了。


2.稍显精致的代码实现

      上面这个代码因为在提交时会报超时,所以需要进行逻辑上的删减,去掉多余的部分。这次的代码采用递归来实现,我依然会给出详细的注释。
代码如下:

class Solution {
private:
	int sum;//用来保存最终的糖果数,进行返回
public:
	int candy(vector<int>& ratings) {
		int N = ratings.size();
		
		sum = 0;
		int assignCandy = 1;//初始对每个极小值点分发的糖果数为1
		int start = 0; //第一轮循环的起始下标
		int end = N - 1; //第一轮循环的结束下标
		//这里就不需要 candys 和 flag 数组了哦
		setCandy(ratings, start, end, assignCandy); //递归方法调用

		return sum;

	}
	
	void setCandy(vector<int>& temp,int start,int end, int assignCandy)
	{
		//注意:sum在 一旦找到当前轮次的极小值时,就会和当前的assignCandy相加,从而构成总的糖果数量。

		//如果区间长度为1的话,则它是一个极小值点,分配给它糖果,并将这个糖果数算在总数sum中。
		if (start == end)
		{
			sum += assignCandy;
			return;
		}
		else if (start > end)//如果在两个极小值接连出现时,会出现这种情况, 直接返回即可,因为两个极小值之间没有元素了
			return;

		int i, prev, after;
		int startT = start, endT; //设置新的区间开始节点startT
		//处理第一个数,如果它是极小值的话
		if (temp[start + 1] >= temp[start])
		{
			sum += assignCandy;
			startT = start + 1;
		}

		for (prev = start, i = start + 1, after = start + 2; i <= end-1 ; ++i, ++after, ++prev)
		{
			if (temp[i] <= temp[prev] && temp[i] <= temp[after])
			{
				sum += assignCandy;
				//一旦碰到一个极小值点,首先需要更新一下区间结束节点endT
				endT = i - 1;
				
				//对一个区间进行递归方法调用,注意待分配的糖果数要加1
				setCandy(temp, startT, endT, assignCandy + 1);//注意:这里可能会发生startT大于endT的现象

				//还需要设置下一个区间的开始节点startT
				startT = i + 1;
			}
		}
		//处理最后一个数,如果它是极小值的话
		if (temp[end] <= temp[end - 1])
		{
			sum += assignCandy;
			endT = end - 1;
		}
		else
			endT = end;
		//对一次循环中的最后一个区间进行分析
		setCandy(temp, startT, endT, assignCandy + 1);
	}
};

3.最终的代码实现

      2 中的代码解决了 1 中存在的问题,但是它有一个缺点。
      当测试用例为{20000,19999,19998,19997,…,4,3,2,1}时,这样的逆序数组时,它的时间复杂度会急剧增大,是 2 中时间复杂度的 N 倍。所以,只好在 2 的基础上加上了关于逆序的判断;并且考虑到算法在完全升序排列的样本上时间复杂度会增大,趋于O(N2),所以在加上关于升序的判断,这点可以参考快速排序的缺点。
      整体上代码的时间复杂度为O(lnN),空间复杂度为O(lnN),性能都还可以。得到最终的代码如下:

class Solution {
private:
	int sum;
public:
	int candy(vector<int>& ratings) {
		int N = ratings.size();

		sum = 0;
		int assignCandy = 1;
		int start = 0;
		int end = N - 1;

		setCandy(ratings, start, end, assignCandy);

		return sum;

	}

	void setCandy(vector<int>& temp,int start,int end, int assignCandy)
	{

		if (start == end)
		{
			sum += assignCandy;
			return;
		}
		else if (start > end)
			return;
		//如果区间中的元素是降序排列的,那么直接计算sum的值即可。 采用等差数列求和公式来计算。
		if (isSorted_descend(temp, start, end)) 
		{
			int j = end - start; //j+1个数。
			int temp = (j + 1)*assignCandy + ((end-start)*(j + 1)) / 2;
			sum += temp;
			return;
		}
		else if (isSorted_ascend(temp, start, end)) //判断是否为升序
		{
			int j = end - start; //j+1个数。
			int temp = (j + 1)*assignCandy + ((end-start)*(j + 1)) / 2;
			sum += temp;
			return;
		}

		int i, prev, after;
		int startT = start, endT;
		if (temp[start + 1] >= temp[start])
		{
			sum += assignCandy;
			startT = start + 1;
		}

		for (prev = start, i = start + 1, after = start + 2; i <= end-1 ; ++i, ++after, ++prev)
		{
			if (temp[i] <= temp[prev] && temp[i] <= temp[after])
			{
				sum += assignCandy;

				endT = i - 1;
				setCandy(temp, startT, endT, assignCandy + 1);
				startT = i + 1;
			}
		}

		if (temp[end] <= temp[end - 1])
		{
			sum += assignCandy;
			endT = end - 1;
		}
		else
			endT = end;
		setCandy(temp, startT, endT, assignCandy + 1);
	}
	//判断当前区间的元素是否为降序排列。如果是,返回 1   如果两个元素相同,则认为非降序
	//结果为真表示是一个严格的降序序列
	int isSorted_descend(vector<int>& temp, int start, int end)
	{
		while (start < end)
		{
			if (temp[start] <= temp[start + 1])
				return 0;
			start++;
		}
		return 1;
	}
	//对是否是一个严格的升序序列的判定
    int isSorted_ascend(vector<int>& temp, int start, int end)
	{
		while (start < end)
		{
			if (temp[start] >= temp[start + 1])
				return 0;
			start++;
		}
		return 1;
	}


};

4.提交结果

      以 3 中的代码进行提交,结果如图:
little_ant_

总结

      最终的代码实现整体上采用了递归的方法,也包含到了贪心和分治的思想。总算是按照自己的思路写完了,挺费劲的,道路是曲折的,但是结局是美好的~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值