P1120 小木棍(写给dfs小佬)

小木棍

传送门:

P1120 小木棍 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路

看到数据范围一般来说就是用dfs了,同时肯定是需要剪枝的

先看DFS思路吧

无非两种思路:

  1. 枚举原木长度 凑相同长的原木来找可以凑多少根原木,原木数越大,单根原木长度越短(这里称原来没被锯断的木头为原木)

  2. 枚举原木长度 计算出每次如果有解的话原木数量为多少,然后凑原木数

    两者虽然都最后与原木数息息相关,但是思路1存在诸多问题,比如什么时候是dfs的终止情况,什么时候应该回溯,这里如果我们枚举的时候从小到大枚举,不依靠原木数是可以以当前每一个木头都被用来组成原木而作为dfs终止条件的,但是带来的问题是这样做的话剪枝所带来的时间优化很少,我暂时是没想到的,而且判断种植的时间复杂度也高,显然是不合理的

    第一点是刚刚看到这道题所想的,但是再转念一想很蠢!很蠢!很蠢!

    第二点的优势就在于用类似于组合数的爆搜来做十分好懂,如果不卡常不看剪枝这道题就是普及-!

ok!开搜

//rep(i,a,b)==for(int i = a; i <=b; i++)	
	rep(i,a[1],sum){//这边先对a数组做一个从大到小的排序,这样原木的最小长度至少也是这里木棍的最大长度,那么原木最大长度毋庸置疑是这里所有木棍之和。
	//为什么?为什么先想着,这个点的思路下面AC Code里写了
		if(sum%i==0){//枚举i,如果正好有sum/i根原木这时候就是有可能有解。
			cnt=sum/i;//如果有解,那么原木就有sum/i根(这边枚举的是原木长度i)
			dfs(1,0,1,i);//开搜
		}
	}
	void dfs(int x,int cur,int start,int len){
	//x表示当前木头组成len的第几根,cur表示但当前已组成的木棍,start表示从第几根木棍开始,len就表示当前情况下的单根原木长度
	//len表示当前单根原木长度
	if(x>cnt){//已找到可以组成的原木数>理想状况下的原木数就是终止条件
		cout<<len;//输出len,因为len是从小到大开始循环的所以能找到的话就是答案
		exit(0);//退出整个程序
		//exit(0)和return 的区别:exit(0)直接退出整个程序,return 是终止当前函数
		//这里找到答案就直接退出整个程序就行了,不用再return回溯浪费时间,这里也算一种剪枝
	}
	if(cur==len){//已组成了一根原木,我们开始组成下一根,
		dfs(x+1,0,1,len);//开始找下一个组成木棍, 从1开始找,当前啥都没有cur是0,len不变
		return;
	}
	rep(i,start,n){//剪枝,肯定是从原来用过木棍的后一个开始找(因为之前已经排序了,所以如果前面大的找不到,那么肯定是在后面小的找)
		if(!st[i] && cur+a[i]<=len){没选择过该木头并且当前木头放进去组不超过要求的原木长
			st[i]=true;//正常的dfs,已选,当前木棍状态变true
			dfs(x,cur+a[i],i+1,len);//当前木棍还没有组合完,所以这里x还是x,当前已选择第i个被锯断的木棍,所以cur=cur+a[i],因为大的已经加过了所以后面肯定要找相对小的了所以这里start=i+1;len不变
			st[i]=false;//状态还
			//因为每次枚举没找到答案的话就需要回溯,但如果加上下面两个if就能大大减少回溯的时间
			if(cur==0) return;//说明当前已经回溯到没有木头组成的状态,那么如果后面小的都无法放进去组,那么前面大的肯定也组不了所以直接return;省时省力,不然还要在for里绕(因为是回溯所以是倒叙的,因为这边我们本来是从大到小找过去的,那么回溯的时候就是从小到大)
			if(cur+a[i]==len) return ;//说明之前已经找到了最优解,回溯的时候直接快速回退,这样至少可以每次回溯减少一个for的时间;
			while(i<n&&a[i]==a[i+1])i++;//按照较小的组成逻辑来说,肯定是大的与小的组而不是大的和大的组小的和小的组,所以相同的直接跳过
		}

	}
	
}

上面的问题都是本人自己写题思考看题解学习,写给每一位跟我一样新入门的同学看!

有问题请评论或私信我,谢谢!

AC code

这边写的被锯断的木头等于上面的木头,就是源数据,下面是为了理顺代码写的,上述写的是思路

// Problem: 
//     P1120 小木棍
//   
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1120
// Memory Limit: 128 MB
// Time Limit: 260 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<iostream>
#include<algorithm>
//#include<cstdio>

#define ll long long 
#define endl '\n'
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>(b);i--)
#define N 70 //1e6+100 
 
using namespace std;
int a[N],n,cnt,sum,MAX;
bool st[N];
bool cmp(int a,int b){
	return a>b;
}
void dfs(int x,int cur,int start,int len){
	//x表示当前被锯断的木棍组成len的第几根,cur表示但前把锯断木棍重组的长度,start表示从第几根木棍开始
	//len表示当前原单根木棍长度
	if(x>cnt){//组成的木棍数(组成的木棍长度为len)大于当前单根原单根木棍长度下的木棍数 就是找到情况
		cout<<len;//输出len,因为len是从小到大开始循环的所以能找到的话就是答案
		exit(0);//退出整个程序
	}
	if(cur==len){//当前被锯断的木棍已经组成一根原木棍,
		dfs(x+1,0,1,len);//开始找下一个组成木棍, 从1开始找,当前啥都没有cur是0,len不变
		return;
	}
	rep(i,start,n){//剪纸,肯定是从原来用过木棍的后一个开始找(因为之前已经排序了,所以如果前面大的找不到,那么肯定是在后面小的找)
		if(!st[i] && cur+a[i]<=len){//如果当前木棍已经与别的木棍(当然恰好的情况下自身木棍长度正好等于len的时候也是一种情况)没有组成过原木棍长度并且选择了当前被锯断的木棍和现在已有的组成木棍相加小于等于当前单根原木棍长度的时候
			st[i]=true;//正常的dfs,已选,当前木棍状态变true
			dfs(x,cur+a[i],i+1,len);//当前木棍还没有组合完,所以这里x还是x,当前已选择第i个被锯断的木棍,所以cur=cur+a[i],因为大的已经加过了所以后面肯定要找相对小的了所以这里start=i+1;len不变
			st[i]=false;//状态还原
			if(cur==0) return;//说明当前组成棍的长度是0,还没有锯断的棍子放进去组成,应该执行本for里的那个dfs,不用回退
			if(cur+a[i]==len) return ;//说明之前已经找到了最优解,回溯的时候直接回退回退到 
			while(i<n&&a[i]==a[i+1])i++;//按照较小的组成逻辑来说,肯定是大的与小的组而不是大的和大的组小的和小的组,所以相同的直接跳过
		}

	}
	
}
int main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	rep(i,1,n){
		cin>>a[i];
		sum+=a[i];
	}
	sort(a+1,a+n+1,cmp);//排序为了方便剪纸
	
	rep(i,a[1],sum){//最小原木棍肯定大于等于最大的被锯断后的木棍,最大原木管肯定小于等于木棍的总和(也就是给出的被锯断后的木棍都是来自于一根木棍)
		if(sum%i==0){//原单根木棍长度肯定是总木棍长度的约数,因为n*len(n表示原木棍数,len表示单根木棍长度,而题目要求是恰好是恰好组成n个木棍时的最短len)
			cnt=sum/i;//当前原单根木棍长度下的原木棍数
			dfs(1,0,1,i);
		}
	}
	
	return 0; 
} 



有问题请评论或私信我,谢谢!

有共同学习需求的可以加入洛谷团队:https://www.luogu.com.cn/team/66731

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值