《剑指offer》每日分享三道题系列-day 5

一 动态规划类型题的初步理解

动态规划是相对比较难理解的类型题,这期博客先给大家分享一些典型的动态规划的类型小题,希望在我的帮助下,大家会对于动态规划有些初步的认识,在后续的系列中,会继续为大家进一步剖析动态规划的相关习题。
动态规划我们需要列出
定义状态f(n),状态转移方程,设置初始值。

1.斐波那锲数列

先来科普一手:
斐波那契数列(Fibonacci数列)是数学家斐波那契以研究兔子繁殖为例研究的数列,故称“兔子数列”,又称为黄金分割数列。
特点就是,从第三个数字开始,这个数字等于他前两个数字的和。
1 1 2 3 5 8 13 ……

(1)迭代的方式

class Solution
{
	public:
	int Fib(int n)
	{
	//如果是第0个数字就是0
		if(n==0)
			return n;
		
		int first=1;
		int second=1;
		int third=1;
		//当n=1,就是当是第一个数字时,就必须返回的是1.也可以特判一下
		while(n-2)//如果要第n个,就需要循环n-2次
		{
			third = first+second;//简单的动态规划思想
			first = second;
			second = third;
			n--;
		}
		return third;
	}
}

(2)递归的方式(map进行剪枝效果)

如果我们要的是第n个数字,那么我们就需要先算出第n-1和第n-2个数字,同理要算出前两个数字,就需要先算出前四个数字。这样使得在我们重复输入我们想要算出的数字时,就会造成重复计算之前的数字的效果,使得整个代码的效率变低。

比如第一次我要算出第8位斐波那锲数列数字,那我就将前7位都需要算出来。那我再要第九位数字,那我就算出前8位数字,这样那前七位数字是不是就被不必要的重复计算了呢?所以,如果实在连续输入需要代码算出第几位数字的时候,能够将前面算过的几位数字进行存储,直接进行访问不用再算了,这样的效率就相对增加了。所以我们用一个map实现刚才我们所说的剪枝的效果。

代码以及注释如下:

class Solution
{
	private:
	unordered_map <int ,int> filter;
	public:
	int Fib(int n)
	{
		if(n==0||n==1) return n;
		if(n==2) return 1;
		
		//先找第前2个,这样下面找前一个的时候减少了查找次数
		int ppre=0;
		if(filter.find(n-2)==filter.end())
		//如果是之前没有算过的数字,就是在filter中没有找到
		{
			ppre=Fib(n-2);
			filter.insert({n-2,ppre});
			//增加一步是将数值进行记录,避免重复计算之前出现的数字,实现剪枝的效果
		}
		else
		{
		//如果找到了,就将它赋值给ppre
			ppre=filter[n-2];
		}
		
		int pre=0;
		if(filter.find(n-1) == filter.end())
		{
			pre=Fib(n-1);
			filter.insert({n-1,pre});
		}
		else
		{
			pre=filter[n-1];
		}
		
		return pre+ppre;
	}
	
}

2.兔子繁衍问题

上面提到过,斐波那锲数列就是建立在兔子繁衍的问题上产生的。
首先我们了解一手题干:

小兔子长到第3个月后每个月又生一对(个)兔子。假如兔子都不死,请问第1个月出生的一 对(个)兔子,至少需要繁衍到第几个月时兔子总数才可以达到N对(个)?

分析:在前两个月中,由于第一个兔子还不具有剩余能力,所以前两个月都是一个兔子。从第三个月开始,第一个兔子开始生育,此时为两只兔子,即第三个月的兔子就等于前两个月的兔子之和。同理,第四个月的兔子之和就等于第二个月的兔子数目加上第三个月的兔子数目。……
具体情况可以自行画图列表理解
代码如下:

int main()
{
    int n=0;
    cin>>n;
    if(n==0)
			return n;
	int first=1;
	int second=1;
	int third=1;
	while(n-2)
	{
		third = first+second;
		first = second;
		second = third;
		n--;
	}
    cout<<third;
    return 0;
}

3.青蛙跳台阶问题

思路:如果在第n级台阶,我可以从哪两个地方来?
只有两个途径,就是从前一级台阶跳上来的或者是从前两节台阶跳上来的

dp,动态规划:
(1). 定义状态 f(n):青蛙跳上第n阶台阶的总跳法
(2) 状态转移方程 f(n)=f(n-1)+f(n-2)
(3). 设置初始值 f(0)=1(如果是0,后面就没法开始) f(1)=1 f(2)=2

int JumpFloor(int target)
{
	if(target==0) return 1;
	if(target<2) return n;
	//做法一:
	/*int ap[]=new int [targrt+1];
	dp[0]=1;
	dp[1]=1;
	dp[2]=2;
	for(int i=2;i<=target;i++)
	{
		dp[i]=dp[i-1]+dp[i-2];
	}
	return dp[target];*/
	
	//做法二:斐波那锲数列思想
	int first=1;
		int second=1;
		int third=1;//当n=1时,就必须返回的是1.也可以特判一下
		while(n-2)//如果要第n个,就需要循环n-2次
		{
			third = first+second;//简单的动态规划思想
			first = second;
			second = third;
			n--;
		}
		return third;
}

4.小矩形覆盖大矩形的问题

思考:如何用n个2乘1 的小矩形无覆盖的去覆盖一个2*n的大矩形
最后一次横着放(一消除消除两个),最后一次竖着放,影响自身一个。

涂掉最后一节矩阵的时候,如果是横着放,那么前一次肯定有一个是横着放的,实现消除两个潜在方式的效果。如果是竖着放,前面的放的方式是不确定的。
在这里插入图片描述

int recover(int number)
{
	int *dp=new int [number+1];
	dp[1]=1;
	dp[2]=2;
	for(int i=3;i<=number;++i)
	{
		dp[i]=dp[i-1]+dp[i-2];
		//可能第i个是横着放的,或者是竖着放的。
	}
	int result=dp[number];
	delete[] dp;
	return result;
}

二. 二叉树镜像变换

首先先画图理解一下二叉树是如何实现镜像变换的,只要理解了原理,其实这道题并不难。
在这里插入图片描述

我们知道,二叉树是递归实现的。那我们只需要在递归调用的过程中,实现左右子树的交换就行。

void Mirror(TreeNode* root)
{
	if(root==NULL)
		return ;
	//进行交换
	TreeNode* temp=root->left;
	root->left=root->right;
	root->right=temp;
	
	Mirror(root->left);
	Mirror(root->right);
}

三. 在一个排序的单链表,存在重复的节点,删除重复的。

简要思路:

先确定重复区间的起始位置,然后确定一个前开后闭的区间,然后开始进行删除的具体操作。

1. 重复区间的表示方法,确定 前开后闭区间 是重点
2. 前后指针的方式来确定重复区间
3. 为避免重复区间在链表首出现,就带一个哨兵位的头结点。
4. 有一个指针指向的是区间的前驱结点保证左开右闭

确定重复区间的范围等操作好理解,在代码中有表示。
具体删除情况的分析如下:
跳出这两个循环,last->next== NULL
//1.没有到尾巴(last->next!=NULL),确定了一段重复的区域需要被删除.(prev,last]->解决方式 prev->next=last。last就是内个尾巴
//2.重复区域到尾巴了(last->next== NULL),也确定了一段重复的区域,last->next== NULL-> 解决方式 prev->next = last(null)
//以上为正常删就行,删的方式也一样
//3.并没有发现重复的区间last->next== NULL,并且到尾巴了。
//-> 不需要处理,自己往后面走,因为到尾巴而退出循环

ListNode* deleteDuplication(ListNode* pHead)
{
	if(pHead==Null)
		return ;
	//新创建一个头结点指向原始链表的头结点
	ListNode* head = new listNode(0);
	head->next=pHead;
	
	ListNode* prev=head;
	listNode* last=prev->next;
	while(last!=NULL)
	{
		while(last->next!=NULL && last->val!=last->next->val)
		{
			prev=last;
			last=last->next;
		}
		//2.确定重复区域
		while(last->next!=NULL && last->val==last->next->val)
		{
			last=last->next;
		}
		//3.开始删除的具体情况的分析解决如下;
		if(prev->next!=last)
		//如果存在重复区间,那么前后指针一定不是挨着的
		{
			prev->next=last->next;
		}
		//不论三种情况怎么处理,都需要将last重新进行赋值处理
		last=last->next;
	}
	return head->next;//去掉我们声明过的头结点
}

四.博主,再来一碗“鸡汤”!

把训练的目标定在永远达不到的地方,但还是要达到你会感到疲累,犹豫,甚至沮丧的程度,但你不许放弃,因为胜利不会是奇迹,有一种天才只从坚定不移的信念中诞生。–科比

在达到目标之前,所有的努力或许都是没有太大回报的。扎根不一定马上找到水,但是想找到水必须得努力地扎根。努力不能没有方向,而方向的确立好听点叫做梦想,幽默点叫做画饼。
让我们拥抱人生理想 的大饼,继续前进吧!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值