小木棍——搜索的剪枝优化

洛谷传送门

题目描述

乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过 50 50 50
现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。
给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。

输入格式

第一行是一个整数 n n n,表示小木棍的个数。
第二行有 n n n 个整数,表示各个木棍的长度 a i a_i ai

输出格式

输出一行一个整数表示答案。

输入样例

9
5 2 1 5 2 1 5 2 1

输出样例

6

数据范围

对于全部测试点, 1 ≤ n ≤ 65 , 1 ≤ a i ≤ 50 1 \leq n \leq 65,1 \leq a_i \leq 50 1n651ai50

题目分析

很显然,这是一道搜索题。
但显然,暴搜是过不去的,所以我们要考虑剪枝。
相信很多小盆友知道这一点,但一不小心就会写错(把根剪掉了),所以,我们需要对照着代码一步一步的分析:

scanf("%d", &n);
for (int i = 1; i <= n; i++)
    scanf("%d", w + i), tot += w[i];
  • 这一部分不用多说,读入小木棍的长度,然后记得统计总长 tot,方便以后做剪枝。
sort(w + 1, w + n + 1, greater<int>());
  • 这里为什么要降序排序呢?因为我们先拼长的再拼短的,可能的方案就会少很多,而且也是方便以后做剪枝。
for (int i = w[1]; i <= tot / 2; i++)
	if (tot % i == 0)
		dfs(1, 0, i, tot / i);
  • 接下来就是正式开始搜索了。在外层先枚举原来长度 i。显然,i 的最小值必然就为所有木棒的长度的最大值 w[1](因为已经排过序了),而最大值应该只会到 tot / 2,因为原来长度必然是总长度的一个因数,而 tot / 2 + 1 ~ tot - 1 之间不存在 tot 的因子。
void dfs(int pre, int sum, int LEN, int target)
  • 我们继续来看 dfs 里的内容。
  • 第一个参数 pre 用于重复性剪枝,代表上一次所选的木棒的下标 +1。因为我们是按从大到小选的,这一次选的木棒应该是小于等于上一次选的,当前只需要从 pre 开始搜即可。
  • 第二个参数 sum 表示的是当前正在拼的木棒拼了多长。
  • 第三个参数 LEN 是个常量,也就是木棒的原来长度。
  • 第四个参数 target 表示还需要拼出多少根木棒。
if (!target)
{
   printf("%d", LEN);
   exit(0);
}
if (sum == LEN)
{
   dfs(1, 0, LEN, target - 1);
   return;
}
  • 这里是两个显而易见的边界条件,如果 target 0 0 0,也就是所有的木棒拼完了,由于我们的 LEN 是从小到大枚举的,直接输出 exit(0); 即可。
  • 对于第二个,如果当前拼出的木棒长度恰好与原来木棒长度相等,那么重新开始拼一根新的木棒。
if (LEN - sum < w[n])
   return;
  • 这个剪枝也十分的好理解,如果当前拼的长度与目标长度之差比 w[n](可以理解为所有木棒长度的最小值)还要小,那么必然不可能拼出 LEN 长度的木棍。还需要注意,这段可行性剪枝应该放在上一段的下面。
for (int i = pre; i <= n; i++)
    if (!vis[i] && sum + w[i] <= LEN)
    {
        vis[i] = true;
        dfs(i + 1, sum + w[i], LEN, target);
        vis[i] = false;
        if (!sum || sum + w[i] == LENi)
            break;
        while (w[i] == w[i + 1])
            i++;
    }
  • 这里是程序的核心部分,也是最难理解的部分了。
  • 首先,第 1 行,枚举的木棒应该是从 pre ~ n
  • 接着,我们用一个 vis 数组标记该木棒是否被选过,如果未被选过并且当前拼的长度+这根木棍的长度不超过总长 LEN,那么我们拼上这根木棍并进入下一层 dfs 去尝试。
  • 搜索完之后回溯,并撤销 vis 标记。

接着,如果当前 sum 0 0 0 或者 sum + w[i]LEN 时直接跳出循环,为什么呢?

  • 既然出现了回溯,那么必然拼这根木棍无法达到目标。
  • sum0 时,代表这是拼木棍的第一根棒子,并且后面无论怎么拼都不行,那么当前这一根换成其他的当然也就不行。
  • sum + w[i]LEN 时,意味着这是拼木棍的最后一根棒子,而后面怎么拼也都不行,那换成其他的棒子来拼也依旧不行。

接着,由于长度为 w[i] 的棒子拼上去不行,与它长度相等的拼上去也不行,所以 i 需要继续往后跳。

一般情况下,如果你将上面所有的代码结合起来,这道题就应该可以 A 掉了,但在洛谷上,却只有 87 分。因此还要在添加一个小小的优化:

  • 刚刚最后一个剪枝:w[i] 不行就找下一个与 w[i] 长度不相等的棍子来拼,我们仍需要将 i 一点一点的挪动,为何不能先预处理出每一根棍子后第一根与它长度不相等的棍子的下标呢?
for (int i = 1; i <= n; i++)
{
    int pos = i + 1;
    while (w[pos] == w[i] && pos <= n)
        pos++;
    nxt[i] = pos;
}
  • 这样我们只需要将刚才的 while 语句改成 i = nxt[i] - 1(由于 i 本身还会往后挪所以要 -1),就可以勉强通过了。

AC代码

#include 
using namespace std;
#define INF 0x3f3f3f3f
int n, tot, maxV, minV = INF, w[65], nxt[65];
bool vis[65];

void dfs(int pre, int sum, int LEN, int target)
{
    if (!target)
    {
        printf("%d", LEN);
        exit(0);
    }
    if (sum == LEN)
    {
        dfs(1, 0, LEN, target - 1);
        return;
    }
    if (LEN - sum < w[n])
        return;
    for (int i = pre; i <= n; i++)
        if (!vis[i] && sum + w[i] <= LEN)
        {
            vis[i] = true;
            dfs(i + 1, sum + w[i], LEN, target);
            vis[i] = false;
            if (sum + w[i] == LEN || !sum)
                break;
            i = nxt[i] - 1;
        }
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", w + i), tot += w[i];
    sort(w + 1, w + n + 1, greater<int>());
    for (int i = 1; i <= n; i++)
    {
        int pos = i + 1;
        while (w[pos] == w[i] && pos <= n)
            pos++;
        nxt[i] = pos;
    }
    for (int i = w[1]; i <= tot / 2; i++)
        if (tot % i == 0)
            dfs(1, 0, i, tot / i);
    printf("%d", tot); // 记得最后如果所有情况都不行的话要输出总长
    return 0;
}

好了,那么这篇博客就到这里了。如果觉得写的好的话,还可以点赞+收藏哦 ^ ⌣ ^ \hat{}\smile\hat{} ^^

你可能感兴趣的:(剪枝,深度优先,算法)