搜索EX(1):P1120 小木棍 [数据加强版]——各种剪枝技巧与最优化剪枝框架

P1120 小木棍 [数据加强版]

在这里插入图片描述

输入输出样例
输入 #1复制
9
5 2 1 5 2 1 5 2 1
输出 #1复制
6

结果这道题最后还是没有做出来,也不想再去做了,先把一些收获写下来好了

总结目录

1 本题使用的剪枝技巧,常用剪枝技巧总结
2 提炼出来的剪枝框架

1常用剪枝技巧总结

本题中,搜索的一个关键是如何确定搜索的空间,也就是我们确定了什么,要去搜索什么。在这题里面,我们需要确定的是搜索的下限,即木棒的最长的长度,木棒的上限,即木棒的长度和。然后去遍历它。这样遍历之后就可以进行具体的搜索了。

暴力搜索会导致TLE,因此要使用剪枝。这里总结一下本题的一些剪枝的思想:

1 如果找到了答案,使用一个bool flag变量,在dfs中,在开头先判断这个变量,以此进行剪枝
2 根据题目的特点确定优先搜索的顺序。比如在本题中,我们对木棒的长度进行排序,我们认为从最长的开始搜索会更快的搜到我们的答案。这个思路在 P1074 靶形数独 中也是有用到过的。
3 根据题目的特点,在遍历长度的时候,我们需要考虑是否整除,否则是没有遍历的意义的,即对dfs参数进行筛选。这个其实相当于我在 滑雪 那题里面我自己做的一个预处理。
4 使用跳跃枚举。一般的枚举都是从1~n,i++。可以使用一个打表的方法,即用数组记录下来,每次记录下循环跳转的路径即可

5 基于排序后的优化,可以引申出其他的优化。

5.1 预处理相同的木棒长度,然后再for循环中直接跳转到下一个序号。
5.2 使用二分法进行查找。 查找的过程,要考虑是找恰好,还是左边界,还是右边界;在边界内还是边界外。这个可以参考我的文章
二分查找练习——leetcode 34. 在排序数组中查找元素的第一个和最后一个位置
https://blog.csdn.net/qq_35299223/article/details/101994242
6 基于最优化情况的剪枝这个情况是指,对于特定的情况,我们可以确定当前的方案已经是最优了。如果当前的方案已经是错误的,那么我们直接返回上一层即可,不需要再进行其他的枚举了。 这个枚举的框架在下面说明。

2 最优化剪枝框架

dfs(){
		if(达到返回要求){
			flag=true;
			return;
		}
		if(flag==true){
			return;//找到了答案,直接剪枝
		}

		for( i= findbound();i<=n; i=Nextind()){
			//上面两个i的初始化以及i的跳跃是在有序情况下可以考虑的优化	
			if(属于最优情况){
				标记
				dfs深搜更深一层
				清除标记
				break或者return直接返回;//因为当前已经是最优化了,因此考虑直接跳出
			}
			else if(不属于最优化情况){
				标记
				dfs深搜更深一层
				清除标记
				//此处不需要跳出,直接进行本层的循环
				//也可以根据情况添加其他的剪枝
			}
		}
}

暴力dfs代码

#include<iostream>
#include<algorithm>
#define maxsize 100
using namespace std;
int n;
int rod[maxsize];
int used[maxsize];
int maxlen;
int minres;
int cnt;
bool flag = false;

bool cmp(int a, int b) {
	return a > b;
}

void dfs(int curcnt, int left, int target) {
	//正在收入第cnt根木棒,当前距离target长度还有left长
	if (curcnt > cnt&&left==target) {
		flag = true;
		minres = target;
		return;//如果已经收完所有木棒,并且收完最后一根后长度恰好,则完成搜索
	}

	for (int i = 1; i <= cnt; i++) {
		if (flag == true) {
			break;//剪枝
		}
		if (used[i] != 1&&left>=rod[i]) {
			used[i] = 1;
			if (left - rod[i] == 0) {
				dfs(curcnt + 1, target, target);//如果恰好接上一根,那么要开始另外一根
			}
			else if (left - rod[i] > 0) {
				dfs(curcnt + 1, left - rod[i], target);//如果无法接上一根,那么继续接
			}
			used[i] = 0;
		}
	}
}


int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		int len;
		cin >> len;
		if (len <= 50) {
			cnt++;
			maxlen = max(maxlen, len);//求最大值,方便后续计算
			rod[cnt] = len;
		}
	}

	int lensum=0;
	for (int i = 1; i <= cnt; i++) {
		lensum += rod[i];
	}

	sort(rod + 1, rod + 1 + cnt, cmp);
	for (int tmplen = maxlen; tmplen <= lensum; tmplen++) {
		dfs(1, tmplen, tmplen);
	}

	cout << minres;

	return 0;
}

代码 66%

#include<iostream>
#include<algorithm>
#define maxsize 100
using namespace std;
int n;
int rod[maxsize];
int used[maxsize];
int Next[maxsize];
int NextLeftboard[maxsize];
int StartArr[maxsize];
int maxlen;
int minres;
int cnt;
bool flag = false;

bool cmp(int a, int b) {
	return a > b;
}

int findrightboard(int arr[],int arrlen, int target) {
	//返回降序数组中第一个小于target的数的序号
	//这对于降序数组来说,找到第一个小于的,相当于找右边界,而且是找区间外的,因此不-1
	//如果找第一个大于的,相当于找左边界
	int left = 1;
	int right = arrlen + 1;
	while (left < right) {
		int mid = (left + right) / 2;
		if (arr[mid] == target) {
			left = mid + 1;
		}
		else if (arr[mid] > target) { 
			left = mid+1;
		}
		else if (arr[mid] < target) {
			right = mid;
		}
	}
	if (right == (arrlen+1))return arrlen;
	else return right;
}

int findleftboard(int arr[], int arrlen, int target) {
	//找到降序数组第一个大于target的数
	//这是是找target区间的左边界,而且是在区间外的,这种情况下画数轴判断,需要-1
	int left = 1;
	int right = arrlen + 1;
	while (left < right) {
		int mid = (left + right) / 2;
		if (arr[mid] == target) {
			right = mid;
		}
		else if (arr[mid] > target) {
			left = mid + 1;
		}
		else if (arr[mid] < target) {
			right = mid;
		}
	}
	//跳出时left==right,由于返回-1,要特判0
	if (left == 1)return 1;
	return (left - 1);
}

void GetNextLeftboard() {
	//NextLeftboard[i]为rod[]数组中第一个大于rod[i]长度的木棒长度的下标
	for (int i = 1; i <= cnt; i++) {
		NextLeftboard[i] = findleftboard(rod, cnt, rod[i]);
	}
}

void GetNextArr() {
	//利用排序后的rod[]数组,得到next[]数组
	//next[i]为rod[]数组中第一个小于rod[i]长度的木棒长度的下标
	for (int i = 1; i <= cnt; i++) {
		Next[i] = findrightboard(rod, cnt, rod[i]);
	}
}

//----------------------正文代码-----------------------------//
int FindNextInd(int arr[], int arrlen, int target,int curindex) {
	//找到在降序数组arr[]中,第一个长度小于target的下标
	//寻找右边界,是边界外,返回right
	//要求左边界从curindex进行跳跃,而不是每次都从[1,cnt]进行二分,而是从[i,cnt]进行二分
	//如果考虑到i已经搜索过,代码中可以从i+1开始进行,但是这样要对left特判越界,此处舍弃
	//由于在for循环中,跳出循环要超过cnt,因此此处的代码还需要修改
	int left = curindex;
	int right = arrlen + 1;
	while (left < right) {
		int mid = (left + right) / 2;
		if (arr[mid] == target) {
			left = mid + 1;
		}
		else if (arr[mid] > target) {
			left = mid + 1;
		}
		else if (arr[mid] < target) {
			right = mid;
		}
	}
	return right;
}

int FindStartInd(int arr[], int arrlen, int leftlen,int startind) {
	//这是是用来初始化枚举的开始,令查找从<=leftlen的下标开始查找合适的长度
	//在降序数组arr[]中,找到第一个小于等于leftlen的数
	//这里可以寻找左边界,边界内,返回left,重复的数字区间在dfs中更新i的跳跃的时候处理即可
	int left = startind;//定义左边界,剪枝
	int right = arrlen + 1;
	while (left < right) {
		int mid = (left + right) / 2;
		if (arr[mid] == leftlen) {
			right = mid;
		}
		else if (arr[mid] < leftlen) {
			right = mid;
		}
		else if (arr[mid] > leftlen) {
			left = mid + 1;
		}
	}
	if (left == arrlen + 1)return arrlen;
	return left;
}

void dfs(int curcnt, int left, int target) {
	//正在收入第cnt根木棒,当前距离target长度还有left长
	if (curcnt > cnt&&left==target) {
		flag = true;
		minres = target;
		return;//如果已经收完所有木棒,并且收完最后一根后长度恰好,则完成搜索
	}

	if (flag == true) {
		return;//剪枝
	}

	for (int i = 1; i <= cnt; i++) {
		if (used[i] != 1&&left>=rod[i]) {
			used[i] = 1;
			if (left - rod[i] == 0) {
				dfs(curcnt + 1, target, target);//如果恰好接上一根,那么要开始另外一根
			}
			else if (left - rod[i] > 0) {
				dfs(curcnt + 1, left - rod[i], target);//如果无法接上一根,那么继续接
			}
			used[i] = 0;
		}
	}
}


void dfs2(int curcnt, int leftlen, int target) {
	if (curcnt > cnt&&leftlen == target) {
		flag = true;
		minres = target;
		return;//已经收完所有木棒,并且最后一根后长度恰好,则完成搜索
	}
	if (flag == true) { 
		return;//找到结果直接跳过后续搜索,最普通的剪枝
	}

	for (int i =1; i <= cnt;i++) {
		//i初始化为<=leftlen的下标,不再从1开始枚举;
		//单次枚举失败时,i从当前下标开始进行跳跃,而不是单纯i++;
		//以上2个剪枝是基于有序的数组才能实现

		if (used[i] != 1) {
			if (leftlen-rod[i]==0) {
				//此为当前最优情况,如果该情况失败了,直接返回,不再进行同层的跳跃枚举
				used[i] = 1;
				dfs2(curcnt + 1, target, target);
				used[i] = 0;//如果走到这一步就说明已经失败了直接跳出循环
				break;//此处使用break或者return跳出都可以,我建议用break;
			}
			if (leftlen - rod[i] > 0) {
				used[i] = 1;
				dfs2(curcnt + 1, leftlen - rod[i], target);
				used[i] = 0;//此处失败了,还可以继续进行枚举,不用跳出
				if (leftlen == target)break;//这个剪枝的原因是因为没有拼过,肯定要拼,那么最长的肯定能用上,这里不行就肯定不行了
				while (rod[i] == rod[i + 1]&&i<cnt) {//一种常用的跳跃循环
					i++;//虽然继续枚举,但是我们要找不同的值
					//当前的i是不可行的,我们遍历知道i+1和i不同即可
				}
			}
		}
	}
}


int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		int len;
		cin >> len;
		if (len <= 50) {
			cnt++;
			maxlen = max(maxlen, len);//求最大值,方便后续计算
			rod[cnt] = len;
		}
	}

	int lensum=0;
	for (int i = 1; i <= cnt; i++) {
		lensum += rod[i];
	}

	sort(rod + 1, rod + 1 + cnt, cmp);

	for (int tmplen = maxlen; tmplen <= lensum; tmplen++) {
		if (lensum%tmplen != 0)continue;
		dfs2(1,tmplen, tmplen);
	}

	cout << minres;
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值