贪心

44:通配符匹配

这道题之前用动态规划的方法做过,此处用贪心重做一遍。

动态规划和贪心算法都是一种递推算法
均有局部最优解来推导全局最优解


不同点:
贪心算法:
1.贪心算法中,作出的每步贪心决策都无法改变,因为贪心策略是由上一步的最优解推导下一步的最优解,而上一部之前的最优解则不作保留。
2.贪心法正确的条件是:每一步的最优解一定包含上一步的最优解。

动态规划算法:
1.全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有最优解
2.动态规划的关键是状态转移方程,即如何由以求出的局部最优解来推导全局最优解
3.边界条件:即最简单的,可以直接得出的局部最优解


贪心法的基本思路:
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到某算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:

  1. 不能保证求得的最后解是最佳的;
  2. 不能用来求最大或最小解问题;
  3. 只能求满足某些约束条件的可行解的范围。

实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解 ;


求最优解的问题,从根本上说是一种对解空间的遍历。最直接的暴力分析容易得到,最优解的解空间通常都是以指数阶增长,因此暴力穷举都是不可行的。
最优解问题大部分都可以拆分成一个个的子问题,把解空间的遍历视作对子问题树的遍历,则以某种形式对树整个的遍历一遍就可以求出最优解,如上面的分析,这是不可行的。
贪心和动态规划本质上是对子问题树的一种修剪。两种算法要求问题都具有的一个性质就是“子问题最优性”。即,组成最优解的每一个子问题的解,对于这个子问题本身肯定也是最优的。如果以自顶向下的方向看问题树(原问题作根),则,我们每次只需要向下遍历代表最优解的子树就可以保证会得到整体的最优解。形象一点说,可以简单的用一个值(最优值)代表整个子树,而不用去求出这个子树所可能代表的所有值。
动态规划方法代表了这一类问题的一般解法。我们自底向上(从叶子向根)构造子问题的解,对每一个子树的根,求出下面每一个叶子的值,并且以其中的最优值作为自身的值,其它的值舍弃。动态规划的代价就取决于可选择的数目(树的叉数)和子问题的的数目(树的节点数,或者是树的高度?)。
贪心算法是动态规划方法的一个特例。贪心特在,可以证明,每一个子树的根的值不取决于下面叶子的值,而只取决于当前问题的状况。换句话说,不需要知道一个节点所有子树的情况,就可以求出这个节点的值。通常这个值都是对于当前的问题情况下,显而易见的“最优”情况。 因此用“贪心”来描述这个算法的本质。由于贪心算法的这个特性,它对解空间树的遍历不需要自底向上,而只需要自根开始,选择最优的路,一直走到底就可以了。这样,与动态规划相比,它的代价只取决于子问题的数目,而选择数目总为1。

贪心是每次就选局部最优,此处是指当 s[i]=p[j]或者p[j]='?' 时继续推进(不同于动态规划,因为不要求当前解就是全局最佳解,所以不需要考虑这样匹配,以后能不能成功匹配,所谓“选择数目为1”)。
因为这种情况下可能不是全局最优,所以,要记录下j中’‘出现的位置和这个’'匹配的s的一段的结束位置 ,也就是留下了反悔的余地
或许这样“留余地的做法”能够使贪心也能解最值问题?

对比这道题动态规划和贪心的做法,也能理解下面这句话的含义了。

它对解空间树的遍历不需要自底向上,而只需要自根开始,选择最优的路,一直走到底就可以了。

放代码:

bool isMatch(string s, string p) {
	int l1=s.length(),l2=p.length(),i=0,j=0;
	int staj=-1,match;//staj代表*起点,match代表*匹配的最后一个i 
	while(i<l1)
	{
		if(j<l2&&s[i]==p[j]||p[j]=='?')//注意j<l2!!! 
		{
			i++;
			j++;
		}
		else if(j<l2&&p[j]=='*')//开始*的匹配 
		{
			staj=j;
			j++;
			match=i; //之所以记录match 和下面的"i=match"呼应,当又一次出现不匹配时,可以让i接着上一次*匹配的位置继续匹配 
		}
		//s[i]既不和p[j]匹配 并且 p[j]也不是* ,说明这种情况下行不通,得让*再多匹配一个继续讨论 
		else if(staj!=-1)
		{
			j=staj+1; //j要回到*的下一个重新开始 
			match++; 
			i=match;//i也要在刚刚匹配*的之后重新开始 
		}
		else
			return false;
	}
	//i没了还要判断j是不是“没了” 
	while(j<l2 && p[j]=='*') 
		j++;
        //若p清空,说明匹配
    return j == l2;
}

135:分发糖果

此题的约束:相邻的分高的小孩的糖一定要大于分低的小孩;每个小孩至少一颗糖
贪心思想:

  1. 如果高,就在前面的基础上+1;
  2. 如果等于就1
  3. 如果小于,看前面的是1还是>1 :
    1)如果前面是>1,就给1,说明ratings之前一直在上升还没开始下降
    2)如果前面是1,说明ratings已经开始进入水平或者开始下降了(只要ratings在下降期或水平期, 前一个必定是1,不可能是>1的)
    则这个小孩的糖果数会影响部分前面的小孩的糖果:
    怎么影响呢?
    按照规则来说,此时ratings已经开始下降了,所以糖果数也要从开始下降的地方维持一个单调递减序列,就要判断下降的起点处的糖果数和此时距离下降起点的距离:
    1>如果相等,说明一直到起点处每个小孩都要增加一个糖果,来维持这个单调递减序列
    2>如果大于,说明下降起点处小孩还没必要增加糖果数,也能维持单调递减

为什么要记录下降点?留余地。
因为为了维护单调递减序列,这里面的每个小孩的糖果数都是受后面的影响的。在当时根据贪心思想给出的糖果数可能会不满足后面的要求,所以记录下来,提供修改的机会。(与上一题思想类似)

处于上升期或者水平期的孩子的糖果是不会受后面的影响的,因为规则是高分比低分多糖,对于这些小孩,右边的孩子糖果增加并不破坏二者平衡。所以sta记录的是ratings下降的起点,而不是糖果下降的起点(相同rating的孩子右边的糖果数会下降)

int candy(vector<int>& ratings) {
    int n = ratings.size();
    if(n < 2)return 1;
    vector<int> d(n,1);
    int ans=1;
    int sta=-1;
    for(int i = 1;i < n;i++){
        if(ratings[i]>ratings[i-1])
        {
        	sta=-1;
		    d[i]=d[i-1]+1;
		    ans+=d[i];
		}
        else if(ratings[i]==ratings[i-1])
        {
        	sta=-1;
			ans+=1;
		}
        else
        {
        	if(sta==-1)
        		sta=i-1;
        	if(d[i-1]==1)
        	{
        		if(i-sta==d[sta])
        		{
        			ans+=i-sta;
        			d[sta]++;
        		}
        		else
        			ans+=i-sta-1;
        	}
        	ans+=1;
		}
    }
    return ans;
}

621:Task Schedule
  • method1:贪心思想
    将n+1个task的排列视为一个周期,在这一个周期内不能有相同的task,这时应该将关注点从每个任务怎么安排——>每个周期怎么安排。按贪心的思想,应该优先选择频数大的task,所以要维护一个频数数组F。在进行每一个周期前要对F排序,选择频数最高的n+1个tasks,若不足n+1个用wait来代替(也就是每个周期的时间是雷打不动的n+1,除了最后一个周期可以不用wait填补)
int leastInterval(vector<char>& tasks, int n)
{
	int l=tasks.size(),count=0,ans=0;
	vector<int> cnt(26,0);
	for(int i=0;i<l;i++) 
		cnt[tasks[i]-'A']++;
	while(1)
	{
		sort(cnt.begin(),cnt.end());
		for(int i=25;i>=0;i--)
		{
			if(cnt[i])
			{
				count++;
				cnt[i]--;
			}
			if(count==n+1)
				break;
		}
		if(count==n+1)
			ans+=count;
		else
		{
			if(!count)//已经没有任何任务,可以退出
				break;
			else if(!cnt[25]) //已经到了最后一个周期
				ans+=count;
			else
				ans+=n+1;
		}
		count=0;
	}
	return ans;
}
  • method2:另一种“贪心”,先把频率最高的task(贯穿始终)放好
    将关注点从一个周期回到一个任务,首先把频数最高的task A放好(中间预留n个位子)定下大框架,再将其他task插入两个A中间。这样的好处是,插入task到两个A中间的时候保证了每个task之间都相差n。并且以下面这种方式头尾相连的方式插入:
    在这里插入图片描述
    1、如果预留的位子够多:

    • 如果频数最高的task只有A这一个,绝对不会有task插在最后一个A后面:因为没有一个task和A一样多,也就是task的头尾不可能会撞,最终答案就是(A频-1)*n+A频
    • 频数最高的不止一个,那么最后一个A后面还要插入,在1)的基础上还要加入最后一个A后面的几个

    2、预留的位子不够:可以加,因为前面预留n个座位只是是满足条件的最低要求,此时没有wait,答案就是tasks总长度。

int leastInterval(vector<char>& tasks, int n) 
{
    vector<int> cnt(26,0);
    int l=tasks.size();
	for(int i=0;i<l;i++) 
		cnt[tasks[i]-'A']++;
	sort(cnt.begin(),cnt.end());
	
	int max=cnt[25],maxnum=1;
	for(int i=24;i>=0;i--)
		if(cnt[i]==max)
			maxnum++;
	int blank=(max-1)*n,left=l-max;
	if(blank>=left)
	{
		if(maxnum==1)
			return max+blank;
		else
			return max+blank+maxnum-1;
	}
	else
		return l;
}

630:课程调度

贪心思想:
在满足最晚结束时间一样的前提下,t 小的一定比t大的安排优先级高。
1、将课程按照due time从小到大排序,更新当前能够安排的课程列表和最短时间sum。为什么要按照due time从小到大排序?因为要逐渐增加课程数量,肯定应该要依次从due time小到大的task考虑,并且已经“被放进去”的课程的due time对后面没有任何影响,如果在后面碰到时间更短的按照贪心思想也可以直接替换。并且不能按照时长排序,因为当前时长短的可能due time比较大可以放在后面。
2、遍历时,若d(i) >= sum+ t(i),则贪心地暂记sum=sum+t(i),并将当前任务加入课程列表;若d(i) < sum + t(i),则说明暂时不能增加任务数量,但是可以优化sum:判断t(i)是否小于课程列表中花费时间最长的task(所以课程列表应该是按照时长从大到小排序的优先队列),是则把它替换掉,并相应更新sum(why替换?当前任务的due time较大,不会影响已存在的课程,替换后总时间更短更优,更有利于之后往里面添加更多课程)

bool cmp(const vector<int>& x, const vector<int>& y)
{
    return x[1] < y[1];
}
int scheduleCourse(vector<vector<int> >& courses)
{
    priority_queue<int> pq;
    vector<int> t(2,0)
    sort(courses.begin(), courses.end(), cmp);
    int sum=0;
    for(int i=0;i<courses.size();i++)
    {
    	t=courses[i];
    	if(sum+t[0]<=t[1])
    	{
    		sum+=t[0];
    		pq.push(t[0]);
		}
		else if(pq.size()&&pq.top()>t[0])
		{
			sum+=t[0]-pq.top();
			pq.pop();
			pq.push(t[0]);
		}
	}
	return sum;
}
757:和每个区间交集至少为2

题解
和630有点类似,思路关键在于对初始数组参数的排序方式和更新方式。
关键词:有序的、最优的(有利于下一次添加课程/有利于提高下一个区间重叠度)

402:移掉K位数字得到最小
  • method1:直观想法
    如果要剩下的数字最小,那么数字从左开始的每一位都要尽可能地小,因此我们要从剩余数字的高位开始,在有效范围内找到最小的数字。
    为什么要从高位开始?因为高位的权重比低位权重大,应该不惜一切代价让高伟尽可能小(贪心)
    何为有效范围?从当前下标->使剩下的数字正好为需要数字数目的下标
string removeKdigits(string num, int k) 
{
    int l=num.length();
    if(k==l)
		return "0";
	int s=0,index=0,h=0;//h记录已取的数字数目
	char _min=num[0];
	string ans;
	while(h<l-k)
	{
		for(int i=s;i<h+k+1;i++)
		{
		//找到有效范围内的最小值
			if(num[i]<_min)
			{
				_min=num[i];
				index=i;
			}
		}
		ans+=num[index];
		h++;
		s=index+1;
		_min=num[s];
		index=s;
	}
	int flag=0,i=0;
	//需要去除多余的0
	while(1)
	{
		if(!flag&&(ans[i]!='0'||i==ans.size()-1))//注意不要忽略最终结果是“0”的情况
		{
			ans=ans.substr(i,ans.size()-i);
			break;
		}
		if(!flag&&ans[i]=='0')
			i++;
	}
	return ans;
} 
  • method2:单调栈
    维护一个单调递增栈,因为维护一个单调递增栈就是删掉了所有可能删的数字,已经不能够删除其中任何一个数字了,否则后面的会集体往前挪,absolutely会变大。
string removeKdigits(string num, int k) 
{
    int l=num.length();
    if(k==l)
	return "0";
    stack<char> s;
    for(int i=0;i<l;i++)
    {
    	char t=num[i];
    	while(s.size()&&s.top()>t&&k)
    	{
    		s.pop();
    		k--;
		}
		s.push(t);
	}
	//没有删够数字,此时只能从末尾开始删
	while(k)
	{
		s.pop();
		k--;
	}
	string ans;
    stack<char> ss;
    while(s.size())
    {
        ss.push(s.top());
        s.pop();
    }
    int flag=0; 
	while(ss.size())
	{
		if(ss.top()=='0'&&flag||ss.top()!='0'||!flag&&ss.size()==1)
        {
            ans+=ss.top();
            flag=1;
        }
		ss.pop();
	}
	return ans;
}
765:情侣握手

其实method2群组的思想和method1的 循环交换相似,每不在一个群组就需要链接一次相当于在一个索引列表中交换一次。method2只有一层循环,用一个寻根判断是否是一个群组代替了method1的最外层循环判断是否已经处于一个索引列表中并被纠正

supermarket:

一天只能卖一个商品,并且商品要在due time前卖掉。给出一组商品的profit和due time,求最大利润。

  • method1:并查集(对比上一题):直观的按照profit排序
  • method2:优先队列(对比630):按照due time从大到小排序,同时维持一个按照profit从大到小排序的优先队列表示当前可选的商品。
    code传送门

936:戳印序列

此题用到了贪心的特点:后面的情况不受前面的影响。所以倒过来考虑最后印下的肯定是完整的序列,然后替换成’?'恢复成上一步。
对于这种“能否得到目的序列”的题,倒着来不失为一种办法。

995:K连续位最小翻转次数

对于每个点,是否要翻转,就要考虑他当前的状态:如果状态是1,那么就continue,否则就要从这个点开始翻转。粗暴的做法就是遍历+依次翻转。但是这样做了过多的反转操作,实际上不需要每个都翻转,只需要记下翻转次数即可,所以通过添加记忆化的方式去优化。每个点当前的状态是受前面K-1个元素的影响的,所以要保存它前面k-1个元素的翻转次数

  • method1:通过队列保存当前位置之前的k-1个位置中作为翻转起点的点,当队首元素+K正好是当前索引时,需要pop掉这个,因为这个翻转所涉及的范围恰好不包括当前元素了,并且队列里面剩下的每次翻转都会影响到当前元素
//method   1:时间45%,空间33% 
int minKBitFlips(vector<int>& A, int K) 
{
	int l=A.size(),res=0;
	queue<int> q; //q存储的是该位置之前的k-1个位置中被翻转的位置索引 
//	vector<int> f(l+1,0); 补在需要f数组,因为f数组的作用就是队列的pop 
	for(int i=0;i<l;i++)
	{
		if(q.size()&&q.front()+K==i)
			q.pop();//相当于上一个method中的 now+=f[i]; 
		if((q.size()+A[i])%2==0) 
		{
			if(i+K>l)
				return -1;
			q.push(i);
			res++;
		}
	}
	return res;
 } 
  • method2:开辟一个flag数组标记是否该位置元素曾经是某次翻转的最后的恰好后面一个元素,如果是则标记为-1,否则为0;每次都需要将之前的翻转次数和flag[i]相加得到当前位置的翻转次数(所以-1的作用就是减去在该位置前结束的翻转,相当于method1中的从队列中pop)
//method 2 :时间90% 空间5% 
int minKBitFlips(vector<int>& A, int K) 
{
    int l=A.size();
	int res=0,now=0;//now记录当前位置被翻转的次数 
	vector<int> f(l+K,0);
	for(int i=0;i<l;i++)
	{
		now+=f[i];//如果f[i]=-1说明曾经是一次翻转的结尾的后面一个位置,则该位置累积翻转次数需要-1 
		if((A[i]+now)%2==1)//得到当前位置经过反转后的实际数字 
			continue;
		else
		{
			if(i+K>l)
				return -1;
			else
			{
				res++;
				now++;
				f[i+K]=-1;
			}
		}
	}
	return res;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值