从Counting Partition-数字分割的多种解法看Python

128 阅读8分钟

从Counting Partition-数字分割的多种解法看Python

Partition分割贯穿CS61A课程中,把我学得天昏地暗,这里作一总结。

第一次出现:

L10:Tree Recursion
#Tree Recursion

def count_partitions_treerecur(n,m):
    if n == 0:
        return 1
    elif n < 0:
        return 0
    elif m == 0:
        return 0
    else:
        with_m = count_partitions_treerecur(n-m, m)
        without_m = count_partitions_treerecur(n, m-1)
        return with_m + without_m

image-20250119104542324.png 仅仅(6,4)程度的partition递归,在python-tutor上就跑了393步,如果增加到(10,4),则会直接报错数据大小超过2MB的限制,这其中涉及的大量操作都是对于每个有无最大值m的分割操作返回值的判断。其基本逻辑是,将一个复杂的大问题转化为小问题,至少用到一个m分割(此时待分割n变成n-m),或者以最大分区为m-1开始一个新分割(此时n不变)。之后只需要进行基本条件的判断:

1,待分割n为0,分割成功。

2,待分割n<0,一定是之前的某个步骤出错,分割失败。

3,最大分区为0,无法分割,分割失败。

第二次出现

SICP 2.3.6 Trees

先了解一下python Tree一些构造函数和选择器。

def tree(label,branches=[]):
    for branch in branches:
        assert is_tree(branch)
    return [label]+list[branches]

def label(tree):
    return tree[0]

def branches(tree):
    return tree[1:]

def is_tree(tree):
    if type(tree)!=list or len(tree)<1:
        return False
    for branch in branches(tree):
        if not is_tree(branch):
            return False
    return True

def is_leaf(tree):
    return not branches(tree)

现在请大家思考,如何将数字分割表现在树中。

Partition trees. Trees can also be used to represent the partitions of an integer. A partition tree for n using parts up to size m is a binary (two branch) tree that represents the choices taken during computation. In a non-leaf partition tree:

  • the left (index 0) branch contains all ways of partitioning n using at least one m,
  • the right (index 1) branch contains partitions using parts up to m-1, and
  • the root label is m.

注意:根节点为m

The labels at the leaves of a partition tree express whether the path from the root of the tree to the leaf represents a successful partition of n.

>>> def partition_tree(n, m):
        """Return a partition tree of n using parts of up to m."""
        if n == 0:
            return tree(True)
        elif n < 0 or m == 0:
            return tree(False)
        else:
            left = partition_tree(n-m, m)
            right = partition_tree(n, m-1)
            return tree(m, [left, right])
>>> partition_tree(2, 2)
[2, [True], [1, [1, [True], [False]], [False]]]

Printing the partitions from a partition tree is another tree-recursive process that traverses the tree, constructing each partition as a list. Whenever a True leaf is reached, the partition is printed.

>>> def print_parts(tree, partition=[]):
        if is_leaf(tree):
            if label(tree):
                print(' + '.join(partition))
        else:
            left, right = branches(tree)
            m = str(label(tree))
            print_parts(left, partition + [m])
            print_parts(right, partition)
>>> print_parts(partition_tree(6, 4))
4 + 2
4 + 1 + 1
3 + 3
3 + 2 + 1
3 + 1 + 1 + 1
2 + 2 + 2
2 + 2 + 1 + 1
2 + 1 + 1 + 1 + 1
1 + 1 + 1 + 1 + 1 + 1

书中用树的左右分支表示前面提到的两种选择,如果从一棵树(子树)的根到叶结点相加等于n,那么在它的叶节点后加上True节点,标记一条正确的路径。False同理。打印时,只需要判断叶结点的bool值就可以展现出分割方法。

第三次出现

SICP 2.3.7 Linked Lists

先了解一下python Linked List一些构造函数和选择器。

>>> empty = 'empty'
>>> def is_link(s):
        """s is a linked list if it is empty or a (first, rest) pair."""
        return s == empty or (len(s) == 2 and is_link(s[1]))
>>> def link(first, rest):
        """Construct a linked list from its first element and the rest."""
        assert is_link(rest), "rest must be a linked list."
        return [first, rest]
>>> def first(s):
        """Return the first element of a linked list s."""
        assert is_link(s), "first only applies to linked lists."
        assert s != empty, "empty linked list has no first element."
        return s[0]
>>> def rest(s):
        """Return the rest of the elements of a linked list s."""
        assert is_link(s), "rest only applies to linked lists."
        assert s != empty, "empty linked list has no rest."
        return s[1]
    
#迭代实现
>>> def len_link(s):
        """Return the length of linked list s."""
        length = 0
        while s != empty:
            s, length = rest(s), length + 1
        return length
>>> def getitem_link(s, i):
        """Return the element at index i of linked list s."""
        while i > 0:
            s, i = rest(s), i - 1
        return first(s)
    
#递归实现
>>> def len_link_recursive(s):
        """Return the length of a linked list s."""
        if s == empty:
            return 0
        return 1 + len_link_recursive(rest(s))
>>> def getitem_link_recursive(s, i):
        """Return the element at index i of linked list s."""
        if i == 0:
            return first(s)
        return getitem_link_recursive(rest(s), i - 1)
    
>>> def extend_link(s, t):
        """Return a list with the elements of s followed by those of t."""
        assert is_link(s) and is_link(t)
        if s == empty:
            return t
        else:
            return link(first(s), extend_link(rest(s), t))
        
>>> extend_link(four, four)
[1, [2, [3, [4, [1, [2, [3, [4, 'empty']]]]]]]]

>>> def apply_to_all_link(f, s):
        """Apply f to each element of s."""
        assert is_link(s)
        if s == empty:
            return s
        else:
            return link(f(first(s)), apply_to_all_link(f, rest(s)))
        
>>> apply_to_all_link(lambda x: x*x, four)
[1, [4, [9, [16, 'empty']]]]

>>> def keep_if_link(f, s):
        """Return a list with elements of s for which f(e) is true."""
        assert is_link(s)
        if s == empty:
            return s
        else:
            kept = keep_if_link(f, rest(s))
            if f(first(s)):
                return link(first(s), kept)
            else:
                return kept
            
>>> keep_if_link(lambda x: x%2 == 0, four)
[2, [4, 'empty']]

>>> def join_link(s, separator):
        """Return a string of all elements in s separated by separator."""
        if s == empty:
            return ""
        elif rest(s) == empty:
            return str(first(s))
        else:
            return str(first(s)) + separator + join_link(rest(s), separator)
        
>>> join_link(four, ", ")
'1, 2, 3, 4'

在链表表示中,我们将递归过程视为依次嵌套连接的链表,比如partitions(6,4)的结果就是这样:

link(
    link(4, link(2, empty)),
    link(
        link(4, link(1, link(1, empty))),
        link(
            link(3, link(3, empty)),
            link(
                link(3, link(2, link(1, empty))),
                link(
                    link(2, link(2, link(2, empty))),
                    link(
                        link(2, link(2, link(1, link(1, empty)))),
                        link(
                            link(2, link(1, link(1, link(1, empty)))),
                            link(1, link(1, link(1, link(1, link(1, empty))))))
                        )
                    )
                )
            )
        )
    )


代码实现:

>>> def partitions(n, m):
        """Return a linked list of partitions of n using parts of up to m.
        Each partition is represented as a linked list.
        """
        if n == 0:
            return link(empty, empty) # A list containing the empty partition
        elif n < 0 or m == 0:
            return empty
        else:
            using_m = partitions(n-m, m)
            with_m = apply_to_all_link(lambda s: link(m, s), using_m)
            without_m = partitions(n, m-1)
            return extend_link(with_m, without_m)
        
>>> def print_partitions(n, m):
        lists = partitions(n, m)
        strings = apply_to_all_link(lambda s: join_link(s, " + "), lists)
        print(join_link(strings, "\n"))
        
>>> print_partitions(6, 4)
4 + 2
4 + 1 + 1
3 + 3
3 + 2 + 1
3 + 1 + 1 + 1
2 + 2 + 2
2 + 2 + 1 + 1
2 + 1 + 1 + 1 + 1
1 + 1 + 1 + 1 + 1 + 1

第四次出现

Lecture 17 : Generators

在python中,函数用yield返回一个生成器对象,可以用来迭代。配合next(iterator)yield会惰性执行函数。(完成一次执行之后暂停,知道要求下一个值)。

也就是说:在本例中,countdown函数会返回一个生成器对象(迭代器)

def countdown(k):
    if k>0:
        yield k
        yield countdown(k-1)
t = countdown(3)
print(next(t))  # 输出 3
print(next(t))  # 输出 countdown(2)(一个迭代器对象)
print(next(next(t)))  # 进入 countdown(2) 迭代器,输出 2
A yield from statement yields all values from an iterator or iterable (Python 3.3)

而使用yield from时:可以正常输出

def countdown(k):
    if k>0:
        yield k
"""另一种写法是for循环:
		for x in countdown(k-1):
			yield k				"""
        yield from countdown(k-1)
t = countdown(3)

再看几个yield from示例

def prefixes(s):
    if s:
        yield from prefixes(s[:-1])
        yield from s
>>>list(prefixes('both'))
['b','bo','bot','both']

def substrings(s):
    if s:
        yield from prefixes(s)
        yield from substrings(s[1:])
>>>list(substrings('tops'))
['t','to','top','tops','o','op','ops','p','ps','s']

在开始yield_partiton之前,我们先对之前的partition进行几次升级。

def count_partition(n,m):
    """Count partitions.
    >>> count_partitions(6,4)
    9
    """
    if n < 0 or m == 0:
        return 0
    else:
        exact_match = 0
        if n == m:
            exact_match = 1
        with_m = count_partition(n-m,m)
        without_m = count_partition(n,m-1)
        return exact_match + with_m + without_m

def list_partition(n,m):
    """list partitions.
>>> for p in list_partitions(6,4) : print(p)
[2, 4]
[1, 1, 4]
[3, 3]
[1, 2, 3]
[1, 1, 1, 3]
[2, 2, 2]
[1, 1, 2, 2]
[1, 1, 1, 1, 2]
[1, 1, 1, 1, 1, 1]
    """
    if n < 0 or m == 0:
        return []
    else:
        exact_match = []
        if n == m:
            exact_match = [[m]]
        with_m = [p + [m] for p in list_partition(n-m,m)]
        without_m = list_partition(n,m-1)
        return exact_match + with_m + without_m

def strings_partition(n,m):
    """strings partitions.
>>> for p in list_partitions(6,4) : print(p)
2+4
1+1+4
3+3
1+2+3
1+1+1+3
2+2+2
1+1+2+2
1+1+1+1+2
1+1+1+1+1+1
    """
    if n < 0 or m == 0:
        return []
    else:
        exact_match = []
        if n == m:
            exact_match = [str(m)]
        with_m = [p + ' + ' + str(m) for p in strings_partition(n-m,m)]
        without_m = strings_partition(n,m-1)
        return exact_match + with_m + without_m
    
def yield_partition(n,m):
    """yield partitions.
>>> for p in list_partitions(6,4) : print(p)
2 + 4
1 + 1 + 4
3 + 3
1 + 2 + 3
1 + 1 + 1 + 3
2 + 2 + 2
1 + 1 + 2 + 2
1 + 1 + 1 + 1 + 2
1 + 1 + 1 + 1 + 1 + 1
    """
    if n > 0 and m > 0:
        if n == m:
            yield str(m)
        for p in yield_partition(n-m,m):
            yield p + ' + ' + str(m)
        yield from yield_partition(n,m-1)

使用yield的好处在于:我们只需要关注当前的元素,当你需要在某个位置停止它,我们可以控制它执行的次数。而不是想之前的partiton函数,只能一次性输出完所有的结果。在交互模式中的运行结果:

#例如partition(60,50)结果将近100万中,我们只想取其中10种会很快。
>>> s = list(yield_partition(60,50))
>>> len(s)
966370
>>> s[0]
'10 + 50'
>>> s[2025]
'1 + 2 + 4 + 13 + 40'
>>> next(yield_partition(60,50))
'10 + 50'
>>> t = yield_partition(60,50)

>>> for _ in range(10):
...     print(next(t))
...
10 + 50
1 + 9 + 50
2 + 8 + 50
1 + 1 + 8 + 50
3 + 7 + 50
1 + 2 + 7 + 50
1 + 1 + 1 + 7 + 50
4 + 6 + 50
1 + 3 + 6 + 50
2 + 2 + 6 + 50

So if you're ever writing a program where there are many possibilities and you only want a few of them, sometimes this generator function approach can not only be easier to write, easier to read, but also dramatically faster to run.

经过对这一问题的思考,我不禁感叹我所知之渺小,这不会是终点,只是对过去的思考和总结,我将继续在学习中完善我对编程的理解。相关代码已经上传GitHub:github.com/kair998/Pyt…

如果对你有帮助的话,请关注我的账号或者星标我的GitHub仓库,我会持续输出学习编程的所思所想。