P1120 小木棍


感谢所有AC

 

传送门

思路

       剪枝典中典。

       先谈朴素的 dfs 。题目需要拼凑多根等长的小木棍,当拼凑完一根木棍时 dfs 还未能结束,应当再继续搜索,拼凑下一根木棍,所以 dfs 中需要一个参数用来记录当前已经完成拼凑的根数或者还剩多少根未拼接,还需要一个参数来表示当前木棍的完成度。当当前木棍完成拼接后,剩余木棍减一,当前木棍清零,继续下一轮搜索。原始木棍的长度范围是已知的,从小到大枚举长度进行搜索,一旦发现符合要求直接输出并 exit(0) 直接退出循环。

       细说剪枝。

       剪枝一:不是所有的原始长度都有搜索的必要,如果木棍总长不能被原始长度整除,那么必然不能拼接成功。

       剪枝二:贪心思想,短木棍相比长木棍更加灵活,因此在拼接木棍时,应当首先考虑长木棍。

       剪枝三:如果当前有一根木棍拼接不成功,那么与它等长的木棍的拼接也不会成功,可以直接跳过。因此可以用一个桶来存储木棍的长度及个数。

       剪枝四:当一根木棍无法拼接成功时就已经没有必要再继续向下搜索了,可以直接回溯。因为如果当前木棍无法拼接成功,说明这样的长度是不符合的。在搜索中我们是优先用长木棍来拼接的,假如在拼接不成功的情况下继续搜索,只会把拼接当前木棍的长木棍替换成若干的短木棍,即使能够拼接完成,在新的木棍中刚刚失败的长木棍也会依旧失败(长木棍迟早要用)。这样的搜索必然是没有结果的,可以直接回溯。

       剪枝五:同剪枝三,如果当前木棍差 x 还没拼接,而木棍群中恰好有木棍长度为 x ,在尝试将 x 拼接到当前木棍上,如果后续木棍无法顺利拼接,则说明当前长度不符合。因为如果继续向下搜索,只会把木棍 x 用若干根小木棍拼接,这样在后续拼接中还是会不成功(依然是贪心思想)。

代码

#include
#include
#include
#define maxn 66
using namespace std;
int a[maxn], len[maxn], pre[maxn], d, n, sum;
void dfs(int u, int k, int p) {
	//u表示当前木棍还有多少长度未拼接
	//k表示当前还剩多少木棍没有拼接
	//p表示当前木棍所需的(拼接进来的木棍的)最短长度
	if (u == 0) {
		dfs(d, k - 1, a[n]);
		//当前木棍拼接完成,继续下一根
		return;
	}
	if (k == 0) {
		cout << d << '\n';
		exit(0);
		//找到答案,任务完成
	}
	p = p < u ? p : u;
	//更新p
	while (p && len[p] == 0)--p;
	//剪枝二,从大到小枚举可行木棍
	while (p) {
		if (len[p]) {
			--len[p];
			dfs(u - p, k, p);
			++len[p];
			if (u == p || u == d)return;
			//剪枝四和剪枝五
		}
		p = pre[p];
		//当前木棍已用完,找下一个长度的木棍
	}
}
int main(void)
{
	ios::sync_with_stdio(false);
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
		sum += a[i];
		len[a[i]]++;
		//剪枝三
	}
	sort(a + 1, a + 1 + n);
	for (int i = 1; i <= n; i++)
		if (a[i - 1] != a[i])
			pre[a[i]] = a[i - 1];
	//一个pre数组,pre[i]记录的是第一个长度小于i的木棍长度
	//在搜索中可以更快速地抵达要拼接的长度
	for (d = a[n]; d << 1 <= sum; d++)
		if (sum % d == 0)
			//剪枝一
			dfs(d, sum / d, a[n]);
	cout << sum << '\n';
	//如果到最后都没有找到答案,说明原始木棍等于总长
	return 0;
}