【Luogu-P1120】 小木棍 [数据加强版]
暴搜 + 毒瘤剪枝。
说说这道花了我三四天才调处来的毒瘤暴搜。
事实证明,该借鉴题解时就要借鉴题解。
不过做完这题之后确实对深搜、优化、剪枝有了更深刻的理解。
题意
给 n n n 个小木棒的长度,求他们最多能拼成的长度相同木棍的长度。
下文为了区分,把短的(用来拼的)叫做(小)木棒,长的(被拼接成的)叫做木棍。
思路
试试我新的思路写法。
1
二分?dp?贪心?似乎都不是。
最后认命地发现它就是一道朴素的暴搜,但却有毒瘤的剪枝。
所以暴搜的大致解法就是:
很明显,我们要先枚举若干根木棍的长度,再拿他来拼接,dfs 判断是否能拼接成功。
每次 dfs 就一根根木棒拼接,看最后是否能拼接成 n / l e n n/len n/len 根的木棍。
注意输入的时候忽略掉那些长度比 50 要大的木棒。
2
先说有点东西的主函数。
首先,在输入了木棒的长度之后要对他们从大到小排序。
易知,相比一根短木棒,长木棒拼接起来会更有限制性,即更短的木棒可以和其他有更多种拼接可能。
也可以理解为短木棒比长木棒更加灵活。
所以,我们在拼接时,就要优先选择长木棒,其次才是短木棒(优化 1)。
其次,可以发现,木棍的长度是有限制的:
-
l e n ∣ s u m len | sum len∣sum(其中 sum 是木棒总长度,len 是当前枚举木棍长度)。
原因很简单,每根木棍的长度相等(题目条件)。
-
l e n ≤ s u m 2 len \le \dfrac{sum}{2} len≤2sum 或 l e n ← s u m len \gets sum len←sum。
因为除了拼接成一根长的木棍之外,木棒至少拼接成两根以上的木棍。
综上所述,
我们只需要枚举 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} m←lensum),
直接
f
l
a
g
←
1
flag \gets 1
flag←1(表示拼接成功),return; 即可。
2
如果当前木棍拼接完成,
我们就需要从剩下的木棒中挑一根最长的木棒去重新拼接一根木棍。
注意前面每使用一根木棒就要标记,挑选没有使用过的木棒去拼接。
记得回溯要取消使用标记!!!
3
当前木棍没有拼接完,
就继续选择小木棒拼接(没有使用过的)。
注意从 l s t + 1 lst + 1 lst+1 开始遍历选择。
4
最后!来说说万恶之源臭名昭著毒瘤至极的剪枝优化。
前两点优化见上文。
dfs 的优化:
-
每次 dfs 一开始就判断 flag 是否等于 1(优化 3):
只需要看当前长度的可行性,所以只要找到一种成功拼接的方式就够了。
-
只要 k = m k=m k=m,就可以直接判定为拼接成功了(优化 4):
如果成功到了最后一根木棍的拼接,此时不用继续了,剩下的木棒一定可以拼接成一根完整的木棍。
因为 l e n ∣ s u m len|sum len∣sum 啊 QwQ。
-
若当前木棍拼接完了,dfs 了下一根的木棍拼接之后,立马判断是否成功,若是,直接返回,其他类似情况也一样(优化 5)。
-
重点:在上文 3.3 中述,若已经继续选择了木棒拼接,失败了,且有 r s t = a i rst=a_i rst=ai,证明已失败,直接返回(优化 6):
当前选择的木棒 a i a_i ai 在此时拼接明显是最优情况(长的优先,且我们是从长到短枚举的)。
若它在此时拼接都失败,证明前面的拼接有误,所以返回。
这也是这道题最难想到的剪枝。
-
每次回溯都要清空 vis 或者 used 数组,这样主函数不需要再 memset,十分省时,可以沿用至其他竞赛(优化 7).
-
在 3.3 中,每次处理完一种长度,下次就可以跳过和它长度相同的木棒了。这个预处理出 nxt 数组,这样不需要每次都 while 求下一个下标(优化 8):
成功不用重复拼接,失败若长度相同也会失败。
-
在优化 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——

浙公网安备 33010602011771号