【Luogu-P1120】 小木棍 [数据加强版]

暴搜 + 毒瘤剪枝。

说说这道花了我三四天才调处来的毒瘤暴搜。

事实证明,该借鉴题解时就要借鉴题解。

不过做完这题之后确实对深搜、优化、剪枝有了更深刻的理解。

题意

n n n 个小木棒的长度,求他们最多能拼成的长度相同木棍的长度。

下文为了区分,把短的(用来拼的)叫做(小)木棒,长的(被拼接成的)叫做木棍。

思路

试试我新的思路写法。

1

二分?dp?贪心?似乎都不是。

最后认命地发现它就是一道朴素的暴搜,但却有毒瘤的剪枝

所以暴搜的大致解法就是:

很明显,我们要先枚举若干根木棍的长度,再拿他来拼接,dfs 判断是否能拼接成功

每次 dfs 就一根根木棒拼接,看最后是否能拼接成 n / l e n n/len n/len 根的木棍。

注意输入的时候忽略掉那些长度比 50 要大的木棒。

2

先说有点东西的主函数。

首先,在输入了木棒的长度之后要对他们从大到小排序。

易知,相比一根短木棒,长木棒拼接起来会更有限制性,即更短的木棒可以和其他有更多种拼接可能。

也可以理解为短木棒比长木棒更加灵活。

所以,我们在拼接时,就要优先选择长木棒,其次才是短木棒(优化 1)


其次,可以发现,木棍的长度是有限制的:

  1. l e n ∣ s u m len | sum lensum(其中 sum 是木棒总长度,len 是当前枚举木棍长度)。

    原因很简单,每根木棍的长度相等(题目条件)。

  2. l e n ≤ s u m 2 len \le \dfrac{sum}{2} len2sum l e n ← s u m len \gets sum lensum

    因为除了拼接成一根长的木棍之外,木棒至少拼接成两根以上的木棍。

综上所述,

我们只需要枚举 len 从 最长木棒长度(最长木棒不能切割,所以木棒必须包含它)

s u m 2 \dfrac{sum}{2} 2sum,每次如果成功拼接,直接输出结束程序,否则最后跳出循环输出 sum(优化 2)。

3

然后说说出场率最高的 dfs。

每次传递三个变量——k、lst、rst,分别表示当前拼接的木棍编号、上一根被拼接的木棒编号,以及拼接完当前木棍还需要的木棒长度。

主要分成 3 个部分。

1

如果拼接成功,

k = m k=m k=m r s t = 0 rst=0 rst=0(其中 m 表示拼接 m 根当前长度的木棍, m ← s u m l e n m \gets \dfrac {sum}{len} mlensum),

直接 f l a g ← 1 flag \gets 1 flag1(表示拼接成功),return; 即可。

2

如果当前木棍拼接完成,

我们就需要从剩下的木棒中挑一根最长的木棒去重新拼接一根木棍。

注意前面每使用一根木棒就要标记,挑选没有使用过的木棒去拼接。

记得回溯要取消使用标记!!!

3

当前木棍没有拼接完,

就继续选择小木棒拼接(没有使用过的)。

注意从 l s t + 1 lst + 1 lst+1 开始遍历选择。

4

最后!来说说万恶之源臭名昭著毒瘤至极的剪枝优化。

前两点优化见上文。

dfs 的优化:

  1. 每次 dfs 一开始就判断 flag 是否等于 1(优化 3)

    只需要看当前长度的可行性,所以只要找到一种成功拼接的方式就够了。

  2. 只要 k = m k=m k=m,就可以直接判定为拼接成功了(优化 4)

    如果成功到了最后一根木棍的拼接,此时不用继续了,剩下的木棒一定可以拼接成一根完整的木棍。

    因为 l e n ∣ s u m len|sum lensum 啊 QwQ。

  3. 若当前木棍拼接完了,dfs 了下一根的木棍拼接之后,立马判断是否成功,若是,直接返回,其他类似情况也一样(优化 5)

  4. 重点:在上文 3.3 中述,若已经继续选择了木棒拼接,失败了,且有 r s t = a i rst=a_i rst=ai,证明已失败,直接返回(优化 6)

    当前选择的木棒 a i a_i ai 在此时拼接明显是最优情况(长的优先,且我们是从长到短枚举的)。

    若它在此时拼接都失败,证明前面的拼接有误,所以返回。

    这也是这道题最难想到的剪枝。

  5. 每次回溯都要清空 vis 或者 used 数组,这样主函数不需要再 memset,十分省时,可以沿用至其他竞赛(优化 7).

  6. 在 3.3 中,每次处理完一种长度,下次就可以跳过和它长度相同的木棒了。这个预处理出 nxt 数组,这样不需要每次都 while 求下一个下标(优化 8)

    成功不用重复拼接,失败若长度相同也会失败。

  7. 在优化 8 中,如果我们直接跳到了 n n n(最后一根木棒),直接结束即可(优化 9)

5

总而言之,此题用上了这些优化就可以飞了~QwQ。

竞赛时打暴力可以沿用这些优化剪枝。

“不过话说普及应该不会考这么毒瘤的深搜吧。。就当学学剪枝了。”——21/08/27

上代码上代码。

ps:上文中的 n n n 在代码中是 c n t cnt cnt + 每一个优化的使用地方均已在注释中标出。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;

#define int long long
#define rint register int
const int maxn = 75;
int n, a[maxn], used[maxn];
int cnt, m, len, sum, minn;
int nxt[maxn];
int flag;

int read ()
{
	int x = 1, s = 0;
	char ch = getchar ();
	while (ch < '0' or ch > '9'){if (ch == '-') x = -1; ch = getchar ();}
	while (ch >= '0' and ch <= '9') s = s * 10 + ch - '0', ch = getchar ();
	return x * s;
}

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

void dfs (int k, int lst, int rst)
{
	int i, j;
	if (flag) return;//优化3
	if (k == m)//优化4
	{
		flag = 1;
		return;
	}
	
	if (rst == 0)
	{
		for (i = 1; i <= cnt; i++)
			if (!used[i])
			{
				used[i] = 1;
				dfs (k + 1, i, len - a[i]);
				used[i] = 0;//优化7
				break;
			}
		if (flag) return;//优化5
	}
	
	for (i = lst + 1; i <= cnt; i++)
	{
		if (!used[i] and rst >= a[i])
		{
			used[i] = 1;
			dfs (k, i, rst - a[i]);
			used[i] = 0;//优化7
			if (flag) return;//优化5
			if (rst == a[i]) return;//重点优化6
			i = nxt[i];//优化8
			if (i == cnt) return;//优化9
		}
	}
}

signed main ()
{
	int i, j;
	n = read ();
	for (i = 1; i <= n; i++)
	{
		int x = read ();
		if (x > 50) continue;
		a[++cnt] = x, sum += x, minn = max (minn, x);
	}
	
	sort (a + 1, a + cnt + 1, cmp);//优化1
	nxt[cnt] = cnt;
	for (i = cnt - 1; i >= 1; i--)//优化8
	{
		if (a[i] == a[i + 1]) nxt[i] = nxt[i + 1];
		else nxt[i] = i;	
	}
	
	for (i = minn; i <= sum + 1 >> 1; i++)//优化2
	{
		if (sum % i == 0) 
		{
			len = i, used[1] = 1, flag = 0;
			m = sum / i;
			dfs (1, 1, len - a[1]);
			if (flag) {printf ("%d\n", len); return 0;}
		}
	}
	printf ("%d\n", sum);
	return 0;
}

—— E n d End End——

posted @ 2022-03-25 07:25  pldzy  阅读(49)  评论(0)    收藏  举报