PrevNext
Warning!

尽管递归并非铜组严格要求的知识,但我们认为把本模块放在铜组而不是银组更合理。

子集

Focus Problem – try your best to solve this problem before continuing!

资料

Resources
CPH

优秀的讲解和代码,无需在此重复

解答——Apple Division

由于 n20n\le 20,可以尝试把 nn 个苹果分入两个集合的所有方案,并找出重量差最小的 一种。下面介绍两种方法。

递归生成子集

第一种方法是编写递归函数,搜索所有可能性。

处理某个下标时,把 weighti\texttt{weight}_i 加入第一个或第二个集合,并用 sum1\texttt{sum}_1sum2\texttt{sum}_2 分别存储两个集合的总重量。

到达数组末尾时,返回两个总和之差。

n = int(input())
weights = list(map(int, input().split()))
def recurse_apples(i: int, sum1: int, sum2: int) -> int:
# We've added all apples- return the absolute difference
if i == n:
return abs(sum2 - sum1)
# Try adding the current apple to either the first or second set

使用位掩码生成子集

Warning!

铜组不要求掌握这部分内容。

位掩码是用二进制表示来代表子集的整数。在本题中,如果某个位掩码的第 ii 位 为 11,就认为第 ii 个苹果属于 s1s_1;否则属于 s2s_2。检查从 002N12^N-1 的所有位掩码,就能遍历 s1s_1 的所有子集。

N=3N=3 为例。下表列出从 002312^3-1 的整数、它们的二进制表示,以及对应 包含在 s1s_1 中的元素。可以看到,所有可能的子集都被涵盖了。

数值二进制s1s_1 中的苹果
0000{}\{\}
1001{0}\{0\}
2010{1}\{1\}
3011{0,1}\{0,1\}
4100{2}\{2\}
5101{0,2}\{0,2\}
6110{1,2}\{1,2\}
7111{0,1,2}\{0,1,2\}

利用这一概念即可实现解法。

你会注意到代码中包含一些位运算:

  • 对整数 xx 而言,1 << x2x2^x 的另一种写法;其二进制表示中只有第 xx 位 为 11
  • &(与)运算符接收两个整数并返回一个新整数。对于整数 aabb,当且仅当 aabb 的第 ii 位都为 11 时,a & b 结果的第 ii 位才为 11。因此, 仅当 mask 的第 xx 位为 11 时,mask & (1 << x) 才返回正值。

如需进一步学习,请参阅专门介绍位运算的模块

n = int(input())
weights = list(map(int, input().split()))
ans = float("inf")
for mask in range(1 << n):
sum1 = 0
sum2 = 0
for i in range(n):
# Checks if the ith bit is set
if mask & (1 << i):

作为可选优化,由于两个集合没有区别,可以规定最后一个数始终位于第二个集合中。 这样只需遍历从 002n112^{n-1}-1 的位掩码,而非到 2n12^n-1

排列

排列是对一列元素的重新排序。

Focus Problem – try your best to solve this problem before continuing!

字典序

这个术语经常出现,例如 USACO 铜组——Photoshoot

想一想字典中的单词如何排序(“字典序”一词正是由此而来)。

在字典中,以字母 a 开头的单词排在最前,随后是以 b 开头的单词,以此类推。 如果两个单词的首字母相同,就比较第二个字母;如果前两个字母也相同,就比较第三个, 如此继续,直到遇到不同字母,或到达某个单词的末尾(此时较短的单词在前)。

排列几乎可以用同样的方法按字典序排序。先按首个元素对排列分组;如果两个排列的 首个元素相同,就比较第二个元素;若第二个也相同,再比较第三个,以此类推。

例如,三个元素的所有排列按字典序为:

[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1].[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1].

注意,列表先列出以 1 开头的排列(就像字典先列出以 a 开头的单词),随后是以 2 和 3 开头的排列。首个元素相同时,用第二个元素进行比较。

通常,除非题目明确要求字典序最小或最大的解,否则不必关心排列是否按字典序生成。 不过,字典序这一概念经常以各种形式出现在算法竞赛题中,因此强烈建议熟悉其定义。

有些题目要求找出满足特定条件的元素顺序。如果 N10N\le 10,可以直接遍历全部 N!=N(N1)(N2)1N!=N\cdot(N-1)\cdot(N-2)\cdots1 个排列,并检查每个排列是否合法。

解答——Creating Strings I

Resources
CPH

对下面两种方法的简要说明和代码

递归生成排列

这只是对 CPH 中方法一的轻微修改。

使用递归函数 search\texttt{search} 找出字符串 ss 的所有排列。首先记录 ss 中每种字符 的数量。每次函数调用时,向当前字符串加入一个可用字符,再以该字符串调用 search\texttt{search}。当当前字符串与 ss 长度相同时,就找到了一个排列,可以将其 加入 perms\texttt{perms} 列表。

s = input()
perms = []
char_count = [0] * 26
def search(curr: str = ""):
# we've finished creating a permutation
if len(curr) == len(s):
perms.append(curr)
return

使用 itertools.permutations 生成排列

itertools.permutations 根据位置而非值区分元素,因此会返回包含重复项的所有排列。 把返回的元组放入集合可以去重;由于它返回元组,还需要把其中的字符拼接成字符串。

from itertools import permutations
s = input()
# perms is a sorted list of all the permutations of the given string
perms = sorted(set(permutations(s)))
print(len(perms))
for perm in perms:
print("".join(perm))

回溯

Focus Problem – try your best to solve this problem before continuing!

资料

Resources
CPH

重点题的代码和讲解

CP2

迭代式与递归式完全搜索

解答——Chessboard & Queens

通过生成排列

检查全部 (648)\binom{64}{8} 种皇后组合的暴力解法需要检查超过 40 亿种布局,因此太慢。

必须更聪明地进行暴力搜索:可以直接生成排列,使任意两个皇后都不会因处于同一行或 同一列而互相攻击。

由于任意两个皇后不能位于同一列,可以在每一列放置一个皇后。接下来只需确定每个 皇后所在的。生成 181\cdots8 的所有排列即可做到,其中每个数字表示对应列的 皇后位于哪一行。

例如,排列 [6,0,5,1,4,3,7,2][6,0,5,1,4,3,7,2] 对应下面的皇后布局:

01234567
0Q
1Q
2Q
3Q
4Q
5Q
6Q
7Q

这样就把需要检查的布局数量降到了更易处理的 8!8!

更简单的对角线检查

为了简化实现,注意从左下到右上的某条对角线可以表示为所有满足 i+j=Si+j=S 的格子 (i,j)(i,j),其中 ii 为行、jj 为列,SS 为某个常数。例如,从 (6,0)(6,0)(0,6)(0,6) 的对角线上,所有格子的坐标之和均为 66

类似地,从右下到左上的对角线也可以这样表示,只需用 iji-j 代替 i+ji+j

from itertools import permutations
DIM = 8
blocked = [[False] * DIM for _ in range(DIM)]
for r in range(DIM):
row = input()
for c in range(DIM):
blocked[r][c] = row[c] == "*"

使用回溯

根据 CPH:

回溯算法从空解开始,一步步扩展解。搜索会递归遍历构造解的所有不同方式。

由于数据范围很小,可以递归回溯皇后的所有放置方式,并存储棋盘的当前状态。

在每一层中,尝试把皇后放在所有未被阻挡、也未受到其他皇后攻击的格子上。随后递归, 再移除这个皇后并回溯。

放置完全部八个皇后时,将答案加一。

DIM = 8
blocked = [[False for _ in range(DIM)] for _ in range(DIM)]
for r in range(DIM):
row = input()
for c in range(DIM):
blocked[r][c] = row[c] == "*"
rows_taken = [False] * DIM
# Indicators for diagonals that go from the bottom left to the top right

题目

StatusSourceProblem NameDifficultyTags
BronzeMedium
Show TagsComplete Search, Recursion, Subsets
BronzeMedium
Show TagsComplete Search, Permutation, Recursion
BronzeMedium
Show TagsComplete Search, Recursion
CCCMedium
Show TagsComplete Search, Permutation
CSESMedium
Show TagsComplete Search, Permutation
CFHard
Show TagsComplete Search, Permutation, Subsets
BronzeVery Hard
Show TagsComplete Search

可以在上面的 CP2 链接或 USACO Training 中找到更多题目。 不过,这类问题如今出现的频率已经远低于过去。

下面代码的时间复杂度是多少?

vector<int> perm(n);
iota(begin(perm), end(perm), 1);
do {
} while (next_permutation(begin(perm), end(perm)));
Question 1 of 4

Module Progress:

PrevNext