【线段树 BFS】LCP 09. 最小跳跃次数

发布于:2025-09-12 ⋅ 阅读:(19) ⋅ 点赞:(0)

本文涉及知识点

C++线段树
C++BFS算法

LCP 09. 最小跳跃次数

为了给刷题的同学一些奖励,力扣团队引入了一个弹簧游戏机。游戏机由 N 个特殊弹簧排成一排,编号为 0 到 N-1。初始有一个小球在编号 0 的弹簧处。若小球在编号为 i 的弹簧处,通过按动弹簧,可以选择把小球向右弹射 jump[i] 的距离,或者向左弹射到任意左侧弹簧的位置。也就是说,在编号为 i 弹簧处按动弹簧,小球可以弹向 0 到 i-1 中任意弹簧或者 i+jump[i] 的弹簧(若 i+jump[i]>=N ,则表示小球弹出了机器)。小球位于编号 0 处的弹簧时不能再向左弹。

为了获得奖励,你需要将小球弹出机器。请求出最少需要按动多少次弹簧,可以将小球从编号 0 弹簧弹出整个机器,即向右越过编号 N-1 的弹簧。

示例 1:
输入:jump = [2, 5, 1, 1, 1, 1]
输出:3
解释:小 Z 最少需要按动 3 次弹簧,小球依次到达的顺序为 0 -> 2 -> 1 -> 6,最终小球弹出了机器。
限制:
1 <= jump.length <= 1 0 6 10^6 106
1 <= jump[i] <= 10000

线段树

性质一:任意解第一步一定是向右弹射。
性质二:任意最优解一定没有两次连续向左弹射。如果有,取消第一次向左弹射。
性质三:任意符合性质一、性质二的最优解,整个操作一定可以拆分成若干以下两类操作单元:操作一,向右弹射。操作二,向右弹射后,马上向左弹射。下面用数学归纳法证明。如果只有一个操作,显然符合性质三。如果第二个操作是右弹,拆出第一个右弹后,仍然符合性质三。如果第二个是左弹,操作数位2,则整个操作就是操作二。如果操作数>2,拆分出前两个操作后,余下的操作仍然符合性质三。
结论一:小球位于i,无论是执行操作一还是操作二到达j,j必定大于i。故从小到大枚举i,可以保证无后效性。
时间复杂度:O(NlogN)。
{ s g [ i ] 是到达 i 的最少次数 i < N s g [ i ] 是到达 [ N . . . ] 的最少测试 i = = N \begin{cases} sg[i]是到达i的最少次数& i< N \\ sg[i]是到达[N...]的最少测试 & i==N\\ \end{cases} {sg[i]是到达i的最少次数sg[i]是到达[N...]的最少测试i<Ni==N

代码

核心代码

倒数第2个样例超时。剪枝后也是如此。样例用的是python。

template<class TSave, class TSet >
class ISegmentTreeCall
{
public:
	virtual void OnUnion(TSave& unionVal, const TSave& leftVal, const TSave& rVal, int leftIndex, int mid, int rIndex) = 0;
};

template<class TSave, class TSet >
class ISingleSegmentTreeCall : public ISegmentTreeCall<TSave,TSet>
{
public:
	virtual void OnUpdateLeaf(TSave& save, int iSaveIndex, const TSet& update) = 0;
};

template<class TSave, class TSet >
class IRangleSegmentTreeCall : public ISingleSegmentTreeCall<TSave, TSet>
{
public:
	virtual void OnUnionSet(TSet& oldBuff, const TSet& newBuff) = 0;
	virtual void OnUpdateBranch(TSave& save, int iLeftIndex, int iRightIndex, const TSet& update) = 0;
};
template<class TSave, class TSet >
class CSegmentTree {
public:
	CSegmentTree(ISegmentTreeCall<TSave, TSet>& call, TSave tDefault)
		:m_call(call), m_tDefault(tDefault) {}
	TSave Query(int left, int r) {
		return Query(left, r, m_tDefault);
	}
	TSave Query(int left, int r, TSave tDefault) {
		TSave ans = tDefault;
		std::function<void(const TSave&, const int&, const int&)> fun = [&](const TSave& cur, const int& leftIndex, const int& rIndex) {
			m_call.OnUnion(ans, ans, cur, left, leftIndex - 1, rIndex);
		};
		Enum(left, r, fun);
		return ans;
	}
	virtual void Enum(int leftIndex, int leftRight, std::function<void(const TSave&, const int&, const int&)>& fun) = 0;
	virtual TSave QueryAll() = 0;
protected:
	ISegmentTreeCall<TSave, TSet>& m_call;
	const TSave m_tDefault;
};

template<class TSave, class TSet >
class CRangSegmentTree :public CSegmentTree<TSave, TSet>
{
public:
	CRangSegmentTree(IRangleSegmentTreeCall<TSave, TSet>& call, TSave tDefault, TSet tRecordNull)
		:m_call(call), CSegmentTree<TSave, TSet>(call, tDefault), m_recordNull(tRecordNull) {}
	virtual void Update(int iLeftIndex, int iRightIndex, TSet value) = 0;
protected:
	IRangleSegmentTreeCall<TSave, TSet>& m_call;
	TSet m_recordNull;
};

template<class TSave, class TSet >
class CRangeVectorSegmentTree : public CRangSegmentTree<TSave, TSet>
{
public:
	CRangeVectorSegmentTree(int iEleSize, IRangleSegmentTreeCall<TSave, TSet>& call, TSave tDefault, TSet tRecordNull) :m_iEleSize(iEleSize), CRangSegmentTree<TSave, TSet>(call, tDefault, tRecordNull)
		, m_save(iEleSize * 4, tDefault), m_record(iEleSize * 4, tRecordNull) {
		CRangSegmentTree<TSave, TSet>::m_recordNull = tRecordNull;
	}
	virtual void Update(int iLeftIndex, int iRightIndex, TSet value) override
	{
		if (max(iLeftIndex, 0) > min(iRightIndex, m_iEleSize - 1)) { return; }//和根节点没交集
		Update(1, 0, m_iEleSize - 1, iLeftIndex, iRightIndex, value);
	}
	virtual void Enum(int iQueryLeft, int iQueryRight, std::function<void(const TSave&, const int&, const int&)>& fun) override {
		if (max(iQueryLeft, 0) > min(iQueryRight, m_iEleSize - 1)) { return; }//和根节点没交集
		Enum(1, 0, m_iEleSize - 1, iQueryLeft, iQueryRight, fun);
	}
	//void Init() {
	//	Init(1, 0, m_iEleSize - 1);
	//}
	TSave QueryAll() override {
		return m_save[1];
	}
	void swap(CRangeVectorSegmentTree<TSave, TSet>& other) {
		m_save.swap(other.m_save);
		m_record.swap(other.m_record);
		std::swap(CRangSegmentTree<TSave, TSet>::m_recordNull, other.m_recordNull);
		std::swap(CSegmentTree<TSave, TSet>::m_tDefault, other.m_tDefault);
		assert(m_iEleSize == other.m_iEleSize);
	}
protected:
	//void Init(int iNodeNO, int iSaveLeft, int iSaveRight)
	//{
	//	if (iSaveLeft == iSaveRight) {
	//		this->OnInit(m_save[iNodeNO], iSaveLeft);
	//		return;
	//	}
	//	const int mid = iSaveLeft + (iSaveRight - iSaveLeft) / 2;
	//	Init(iNodeNO * 2, iSaveLeft, mid);
	//	Init(iNodeNO * 2 + 1, mid + 1, iSaveRight);
	//	this->OnUpdateParent(m_save[iNodeNO], m_save[iNodeNO * 2], m_save[iNodeNO * 2 + 1], iSaveLeft, iSaveRight);
	//}
	void Enum(int iNodeNO, int iSaveLeft, int iSaveRight, int iQueryLeft, int iQueryRight, std::function<void(const TSave&, const int&, const int&)>& fun) {
		if ((iSaveLeft >= iQueryLeft) && (iSaveRight <= iQueryRight)) {
			fun(m_save[iNodeNO], iSaveLeft, iSaveRight);
			return;
		}
		FreshChild(iNodeNO, iSaveLeft, iSaveRight);
		const int mid = iSaveLeft + (iSaveRight - iSaveLeft) / 2;
		if (mid >= iQueryLeft) {
			Enum(iNodeNO * 2, iSaveLeft, mid, iQueryLeft, iQueryRight, fun);
		}
		if (mid + 1 <= iQueryRight) {
			Enum(iNodeNO * 2 + 1, mid + 1, iSaveRight, iQueryLeft, iQueryRight, fun);
		}
	}
	void Update(int iNode, int iSaveLeft, int iSaveRight, int iOpeLeft, int iOpeRight, TSet value)
	{
		if ((iOpeLeft <= iSaveLeft) && (iOpeRight >= iSaveRight))
		{
			UpdateNode(iNode, iSaveLeft, iSaveRight, value);
			return;
		}
		if (iSaveLeft == iSaveRight) {
			return;//没有子节点
		}
		FreshChild(iNode, iSaveLeft, iSaveRight);
		const int iMid = iSaveLeft + (iSaveRight - iSaveLeft) / 2;
		if (iMid >= iOpeLeft)
		{
			Update(iNode * 2, iSaveLeft, iMid, iOpeLeft, iOpeRight, value);
		}
		if (iMid + 1 <= iOpeRight)
		{
			Update(iNode * 2 + 1, iMid + 1, iSaveRight, iOpeLeft, iOpeRight, value);
		}
		// 如果有后代,至少两个后代
		CRangSegmentTree<TSave, TSet>::m_call.OnUnion(m_save[iNode], m_save[iNode * 2], m_save[iNode * 2 + 1], iSaveLeft, iMid, iSaveRight);
	}
	void UpdateNode(int iNode, int iSaveLeft, int iSaveRight, TSet value) {
		if (iSaveLeft == iSaveRight) {
			CRangSegmentTree<TSave, TSet>::m_call.OnUpdateLeaf(m_save[iNode], iSaveLeft, value);//叶子节点
		}
		else {
			CRangSegmentTree<TSave, TSet>::m_call.OnUpdateBranch(m_save[iNode], iSaveLeft, iSaveRight, value);
			CRangSegmentTree<TSave, TSet>::m_call.OnUnionSet(m_record[iNode], value);
		}
	}
	void FreshChild(int iNode, int iDataLeft, int iDataRight)
	{
		if (CRangSegmentTree<TSave, TSet>::m_recordNull == m_record[iNode])
		{
			return;
		}
		const int iMid = iDataLeft + (iDataRight - iDataLeft) / 2;
		UpdateNode(iNode * 2, iDataLeft, iMid, m_record[iNode]);
		UpdateNode(iNode * 2 + 1, iMid + 1, iDataRight, m_record[iNode]);
		m_record[iNode] = CRangSegmentTree<TSave, TSet>::m_recordNull;
	}
	vector<TSave> m_save;
	vector<TSet> m_record;
	const int m_iEleSize;
};

template<class TSave = int, class TSet = int >
class CMinMinSegmentTreeCall :public IRangleSegmentTreeCall <TSave, TSet> {
public:
	virtual void OnUpdateLeaf(TSave& save, int iSaveIndex, const TSet& update)
	{
		save = min(save, update);
	}
	virtual void OnUnion(TSave& unionVal, const TSave& leftVal, const TSave& rVal, int leftIndex, int mid, int rIndex) {
		unionVal = min(leftVal, rVal);
	}
	virtual void OnUnionSet(TSet& oldBuff, const TSet& newBuff) {
		oldBuff = min(oldBuff, newBuff);
	}
	virtual void OnUpdateBranch(TSave& save, int iLeftIndex, int iRightIndex, const TSet& update) {
		save = min(save, update);
	}
};

	class Solution {
		public:
			int minJump(vector<int>& jump) {
				const int N = jump.size();
				CMinMinSegmentTreeCall<int, int> call;
				CRangeVectorSegmentTree<int, int> sg(N + 1, call, INT_MAX / 2, INT_MAX / 2);
				sg.Update(0, 0, 0);
				for (int i = 0;i < N;i++) {
					const int iNew = sg.Query(i, i)+1;
					const int iJump = min(N, i + jump[i]);
					sg.Update(iJump, iJump, iNew);
					sg.Update(i+1, iJump - 1, iNew + 1);
				}
				const int ans = sg.Query(N, N);
				return (ans >= INT_MAX / 2) ? -1 : ans;
			}
		};

单元测试

vector<int> jump;
		TEST_METHOD(TestMethod11)
		{
			jump = { 2, 5, 1, 1, 1, 1 };
			auto res = Solution().minJump(jump);
			AssertEx(3, res);
		}

BFS

vis[i]记录到达i需要的次数。
队列leftMove 记录未被访问的位置,采用懒删除法,当左移时判断 vis[队首]是否已经能到达,如果能出队。

class Solution {
		public:
			int minJump(vector<int>& jump) {
				const int N = jump.size();
				vector<int> vis(N + 1, -1);
				queue<int> que, leftMove;
				for (int i = 0;i < N;i++) {
					leftMove.emplace(i);
				}
				auto Add = [&](int cur, int cnt) {
					if (-1 != vis[cur]) { return; }
					vis[cur] = cnt;
					if (N == cur) { return; }
					que.emplace(cur);
				};
				Add(0, 0);
				while (que.size()) {
					const int cur = que.front();que.pop();
					const int iJump = min(N, cur + jump[cur]);
					Add(iJump,vis[cur] + 1);					
					while (leftMove.size() && (leftMove.front() < cur)) {
						Add(leftMove.front(), vis[cur] + 1);
						leftMove.pop();
					}
				}	
				return vis.back();
			}
		};

扩展阅读

我想对大家说的话
工作中遇到的问题,可以按类别查阅鄙人的算法文章,请点击《算法与数据汇总》。
学习算法:按章节学习《喜缺全书算法册》,大量的题目和测试用例,打包下载。重视操作
有效学习:明确的目标 及时的反馈 拉伸区(难度合适) 专注
员工说:技术至上,老板不信;投资人的代表说:技术至上,老板会信。
闻缺陷则喜(喜缺)是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛
失败+反思=成功 成功+反思=成功

视频课程

先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771
如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

测试环境

操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。


网站公告

今日签到

点亮在社区的每一天
去签到