Skip to content

06648: Sequence

http://cs101.openjudge.cn/practice/06648/

英文版,http://cs101.openjudge.cn/practice/02442/

给定m个数字序列,每个序列包含n个非负整数。我们从每一个序列中选取一个数字组成一个新的序列,显然一共可以构造出n^m个新序列。接下来我们对每一个新的序列中的数字进行求和,一共会得到n^m个和,请找出最小的n个和

输入

输入的第一行是一个整数T,表示测试用例的数量,接下来是T个测试用例的输入 每个测试用例输入的第一行是两个正整数m(0 < m <= 100)和n(0 < n <= 2000),然后有m行,每行有n个数,数字之间用空格分开,表示这m个序列 序列中的数字不会大于10000

输出

对每组测试用例,输出一行用空格隔开的数,表示最小的n个和

样例输入

1
2 3
1 2 3
2 2 3

样例输出

3 3 4

思路:输入时将各条序列sort,先只考虑两条序列,(0,0)一定最小,用heapq存储,下一步最小一定在(i+1, j)和(i, j+1)之间,以此类推找到最小的n个存为序列seq,再将seq与第三条序列重复操作,以此类推。注意m=1的情况。

python
import sys
import heapq

def merge(arr1, arr2, n):
    """
    将两个有序数组 arr1 和 arr2 合并,求出所有组合中最小的 n 个和
    使用堆来进行合并搜索
    """
    heap = []
    visited = set()
    # 初始候选项:(arr1[0]+arr2[0], 0, 0)
    heapq.heappush(heap, (arr1[0] + arr2[0], 0, 0))
    visited.add((0, 0))
    result = []
    while len(result) < n:
        s, i, j = heapq.heappop(heap)
        result.append(s)
        # 如果 arr1 中的下一个数存在,尝试加入候选项
        if i + 1 < n and (i + 1, j) not in visited:
            heapq.heappush(heap, (arr1[i + 1] + arr2[j], i + 1, j))
            visited.add((i + 1, j))
        # 如果 arr2 中的下一个数存在,尝试加入候选项
        if j + 1 < n and (i, j + 1) not in visited:
            heapq.heappush(heap, (arr1[i] + arr2[j + 1], i, j + 1))
            visited.add((i, j + 1))
    return result

def main():
    input_data = sys.stdin.read().split()
    it = iter(input_data)
    T = int(next(it))
    results = []
    for _ in range(T):
        m = int(next(it))
        n = int(next(it))
        # 读取第一个序列,并排序
        current = sorted(int(next(it)) for _ in range(n))
        # 依次与后续的 m-1 个序列合并
        for _ in range(m - 1):
            seq = sorted(int(next(it)) for _ in range(n))
            current = merge(current, seq, n)
        results.append(" ".join(map(str, current)))
    sys.stdout.write("\n".join(results))

if __name__ == "__main__":
    main()

虑到n^m个和的数量可能非常大,我们不能直接存储它们。因此,我们可以通过逐步合并两个序列来找到最小的n个和,而不是一次性生成所有可能的和。

为了找到最小的n个和,我们可以按照以下步骤操作:

  1. 对每个序列进行排序,确保我们可以从最小的元素开始处理。
  2. 使用一个最小堆(优先队列)来维护当前可能的最小和。最初,我们只将每个序列的最小元素(即每个序列的第一个元素)的和放入最小堆中。
  3. 每次从最小堆中取出当前的最小和,然后探索通过替换这个和中的某个元素来得到下一个可能的最小和。
  4. 重复这个过程,直到我们找到了n个最小的和。

以下是一个更为内存效率的Python代码解决方案:

python
import heapq

t = int(input())
for _ in range(t):
    m, n = map(int, input().split())
    seq1 = sorted(map(int, input().split()))
    for _ in range(m - 1):
        seq2 = sorted(map(int, input().split()))

        # 使用最小堆存储可能的最小和以及对应的索引
        min_heap = [(seq1[i] + seq2[0], i, 0) for i in range(n)]
        heapq.heapify(min_heap)
        result = []
        for _ in range(n):
            current_sum, i, j = heapq.heappop(min_heap)
            result.append(current_sum)
            if j + 1 < len(seq2):
                heapq.heappush(min_heap, (seq1[i] + seq2[j + 1], i, j + 1))
        seq1 = result
    print(*seq1)

这个解题思路,类似于机器学习中梯度下降减少loss?

你的观察非常准确!这个解题思路在某种程度上确实类似于机器学习中的梯度下降算法,尤其是在减少“损失”(或目标函数)的过程中。分析两者的相似之处:

梯度下降与当前算法的相似性

  1. 迭代优化

    • 梯度下降:通过迭代地调整参数,逐步减小损失函数的值,最终达到最优解。
    • 当前算法:通过迭代地合并序列,每次选择当前最小的和,逐步优化结果序列。
  2. 局部最优选择

    • 梯度下降:在每一步选择使损失函数下降最快的方向(即梯度的反方向)。
    • 当前算法:使用最小堆(优先队列)每次选择当前最小的和,确保每一步都是局部最优的选择。
  3. 逐步逼近全局最优

    • 梯度下降:通过不断迭代,期望最终达到全局最优(在凸函数的情况下)。
    • 当前算法:通过逐步合并序列,最终得到一个优化后的结果序列。

类比梯度下降的关键点

  • 选择最小损失:在梯度下降中,选择使损失函数下降最快的方向;在这里,选择当前最小的和。
  • 更新参数:梯度下降中更新模型参数;这里更新结果序列seq1
  • 迭代过程:两者都是通过多次迭代逐步逼近最优解。

总结

你的解题思路确实与梯度下降算法在概念上有许多相似之处,尤其是在迭代优化和局部最优选择方面。这种贪心策略通过每一步选择当前最优的选项,最终得到一个整体较优的结果,类似于梯度下降通过每一步的微小调整逐步减少损失函数值。

这种相似性不仅帮助理解算法的工作原理,也展示了不同领域中优化问题的共通性。继续深入理解这些概念,可以帮助你在解决更复杂的问题时更加得心应手!

python
import heapq

def merge_sequences(seq1, seq2, n):
    # 对两个序列进行排序
    seq1.sort()
    seq2.sort()
    # 使用最小堆存储可能的最小和以及对应的索引
    min_heap = [(seq1[i] + seq2[0], i, 0) for i in range(len(seq1))]
    # 生成最小n个和
    result = []
    while n > 0 and min_heap:
        current_sum, i, j = heapq.heappop(min_heap)
        result.append(current_sum)
        if j + 1 < len(seq2):
            heapq.heappush(min_heap, (seq1[i] + seq2[j + 1], i, j + 1))
        n -= 1
    return result

def min_sequence_sums(m, n, sequences):
    # 对所有序列进行排序
    for seq in sequences:
        seq.sort()
    # 逐步合并序列
    current_min_sums = sequences[0]
    for i in range(1, m):
        current_min_sums = merge_sequences(current_min_sums, sequences[i], n)
    return current_min_sums

# 读取输入数据
T = int(input())  # 读取测试用例的数量
for _ in range(T):
    m, n = map(int, input().split())  # 对于每个测试用例,读取m和n
    sequences = [list(map(int, input().split())) for _ in range(m)]
    results = min_sequence_sums(m, n, sequences)
    print(' '.join(map(str, results[:n])))

这段代码定义了两个函数:merge_sequences 用于合并两个已排序的序列并找到最小的n个和,而min_sequence_sums 用于逐步合并所有序列。注意,由于题目要求输出最小的n个和,所以每次合并操作后我们仅保留n个和。这样可以保证内存使用量不会超过题目要求的限制。