Week2Day3A:搜索枚举【2023 安全创客实践训练|笔记】

内容为武汉大学国家网络安全学院2022级大一第三学期“996”实训课程中所做的笔记,仅供个人复习使用,如有侵权请联系本人,将于15个工作日内将博客设置为仅粉丝可见。

目录

搜索枚举

枚举排列

枚举组合

例题选讲

文具店

解析

超级书架 2

解析


搜索枚举

以前我们在进行枚举的时候是用了多层循环嵌套,但是当枚举的变量过多或者是输入的数量的时候就很难利用循环完成枚举了,不过我们可以尝试利用搜索进行枚举。

通常,我们通过一个 dfs 函数来完成搜索枚举,而通过参数表示当前状态。例如在大部分搜索枚举问题中,可以通过 step 或 depth 表示当前枚举层数,或使用 n 表示已经选入的数量,抑或在对于一些对  有限制的问题中,使用 sum 表示已经选入的数量之和。

让我们看一道能够使用搜索枚举实现的题目:现有方程a_1​x_1​+a_2​x_2​+a_3​x_3​+⋯+a_n​x_n​=0,其中2≤n≤10,1≤a_i​≤5,−2≤x_i​≤2,x_i​∈Z,求解的总数。

能够估算所有的状态总数在 5^10≈10^7 ,能够枚举全部的状态。虽然能够使用 10 个循环完成,但此处使用搜索枚举更为方便。

int ans = 0;
void dfs(int dep, int sum) {
    if (dep == n) {
        if (sum == 0){
            ans++;
        }
        return ;
    }
    for (int i = -2; i <= 2; i++) {
        dfs(dep + 1, sum + a[dep] * i);
    }
}

在很多搜索枚举的问题中,会要求我们打印解的具体内容,那么可使用数组来保存具体的解。如对于之前求方程解的问题,可将代码修改为:

int ans[15];
void dfs(int dep, int sum) {
    if (dep == n) {
        if (sum == 0){
            for (int i = 0; i < n; i++) {
                printf("%d ", ans[i]);
            }
            puts("");
        }
        return ;
    }
    for (int i = -2; i <= 2; i++) {
        ans[dep] = i;
        dfs(dep + 1, sum + a[dep] * i);
    }
}

把在dep这一层选择的情况放在ans[dep]位置,ans数组就记下了目前枚举到的情况。

在搜索枚举的过程中,我们能够根据题目的一些性质,对求解的过程进行剪枝优化,这个我们以后也会学到。但是对大部分题目来说,搜索枚举很有可能达到状态的上限,所以很有必要在决定使用搜索枚举之前确定状态的总数。


枚举排列

在之前的搜索枚举中,我们并没有考虑选入物品的 排列顺序。但在一些题目中,会要求考虑给定数字或物品的排列,这种排列可以是在 n 个中的所有符合要求的全排列,也可以是在 n 中找到长度为 k 的排列。

如果使用我们之前的搜索枚举方法,我们发现难以用参数标记原数组中数字的选取情况,那么我们就需要一个全局的布尔数组,帮助我们标记哪些数字已经被选入了排列。另一方面,由于我们使用了这样的全局标记数组,那么必然在搜索时使用到 回溯 技巧,在这个分支的搜索结束后,将标记数组还原。

若要输出 n 个数字全排列,在 dfs 数组中需要的参数需要包含已经选入的数字,在选取当前位数字后进行搜索后,要注意进行回溯

int n;
int per[N];
int vis[N];
void dfs (int dep) {
    if (dep == n) {
        for (int i = 0; i < n; i++) {
            printf("%d ", per[i]);
        }
        puts("");
        return;
    }
    for (int i = 1; i <= n; i++) {
        if(vis[i]) {
            continue;
        }
        vis[i] = 1;
        per[dep] = i;
        dfs(dep + 1);
        vis[i] = 0;
    }
}

如果想要输出 n 个数字的 k 排列,我们可以在之前代码上进行一些较小的修改。当我们选取到 k 个数字时就应该停止继续搜索枚举的过程。


枚举组合

与枚举排列不同,组合中的数字更像是一个集合,即将不同顺序视为一种。很明显,如果使用与之前相似的枚举手段,将会产生大量的重复。可以直接在之前的代码上修改,不过我们这里先教大家用另一种更直接的方法来枚举组合,我们改变枚举搜索的策略。

我们发现通过枚举每一个数选或者不选就可以把所有组合枚举出来了,此时,我们需要的仅仅是知道现在在看第几个,已经选了几个,这两个数就作为参数,假设我们目前考虑到了第x个,目前选了dep个,那么选这个数以后要去的状态就是(x + 1, dep + 1),不选这个数,要去的状态就是(x + 1, dep)

写成代码就是下面这样

int n, k;
int comb[N];
void dfs(int x, int dep) {
    if (dep == k) {
        for (int i = 0; i < k; i++) {
          printf("%d ", comb[i]);
        }
        puts("");
        return;
    }
    if (x > n) {
        return;
    }
    comb[dep] = x;
    dfs(x + 1, dep + 1);
    dfs(x + 1, dep);
}

例题选讲

文具店

蒜头君来到文具店,选择了 k 支自己喜欢的水彩笔,并抄下了它们的价格。可是到结算时,他发现自己抄价格时抄得太密集,以至于所有价格连成了一个数字串。老板想和蒜头君开个玩笑,于是对他说:“你可以把这个数字串分成 k 段,代表这 k 支笔的价格,然后把他们加起来,就是你要付给我的钱了。”

当然,蒜头君想尽可能省下钱去买《算法导论》,所以请你来帮忙算算,他最少需要付多少钱。注意水彩笔的钱可以为 0 元。

数据范围:1≤k≤∣s∣≤8,s 仅包含数字 0∼9。

解析

我们从题目中提取能表示状态的信息,发现如果我们从前往后看这个字符串,那么看了前几位,目前分了几段就是重要的信息,那么我们就可以把这两个信息作为参数,u表示目前看到的位置,cnt表示目前分了几段。

接下来就要看u这一位的数是作为一个新的数的开始,还是接进之前的数里,那我们还需要知道的信息就是目前分出来的数的和是多少,设为sum,当前这最后一个数是多少,设为x,那把这一位接进x里就是x * 10 + (s[u] - '0')

两种情况:

第一种,这一位是一新的数的开始,那接下来到的状态就是(u + 1, cnt + 1, sum + x, s[u] - '0')

第二种,这一位接进之前的数里,那接下来到的状态就是(u + 1, cnt, sum, x * 10 + (s[u] - '0'))

注意第一位必须是一个数新的开始。

注意到最后,每一位都看完以后,把分出来的最后一个数也加进sum里。

超级书架 2

Farmer John 最近为奶牛们的图书馆添置了一个巨大的书架,尽管它是如此的大,但它还是几乎瞬间就被各种各样的书塞满了。现在,只有书架的顶上还留有一点空间。 所有 N(1≤N≤20) 头奶牛都有一个确定的身高 H_i​ (1≤H_i​≤1,000,000)。设所有奶牛身高的和为 S。书架的高度为 B,并且保证 1≤B≤S。 为了够到比最高的那头奶牛还要高的书架顶,奶牛们不得不像演杂技一般,一头站在另一头的背上,叠成一座“奶牛塔”。当然,这个塔的高度,就是塔中所有奶牛的身高之和。为了往书架顶上放东西,所有奶牛的身高和必须不小于书架的高度。 塔叠得越高便越不稳定,于是奶牛们希望找到一种方案,使得叠出的塔在高度不小于书架高度的情况下,高度尽可能小。你也可以猜到你的任务了:写一个程序,计算奶牛们叠成的塔在满足要求的情况下,最少要比书架高多少。

解析

这道题目就是枚举组合的经典应用了,通过枚举所有可能的堆叠情况,就能够确定最少奶牛塔比书架高多少。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值