洛谷-P1118 [USACO06FEB] Backward Digit Sums G/S 题解

题目链接:https://www.luogu.com.cn/problem/P1118

题目大意

给定一个正整数 $N$ 和最终的总和 sum,要求找出字典序最小的排列 a_1, a_2, ..., a_N(其中每个数字是 1 到 N 的一个排列),使得按照如下规则不断合并相邻两项,最后得到的唯一数字等于 sum

  • 每次将相邻两个数相加,生成长度减一的新序列;
  • 重复此过程直到只剩一个数。

例如:

3   1   2   4
  4   3   6
    7   9
     16

目标是从 N 和最终的 sum 反推出原始排列,并输出字典序最小的那个。若无解,则不输出任何内容。

核心观察:组合数学建模

这个过程本质上等价于 杨辉三角(Pascal's Triangle)的加权和

为什么?

每个原始位置上的数在每一轮合并中会“传播”到下一层,其贡献次数恰好等于从顶部走到底部经过该位置的路径数

例如当 N=4 时,系数为:

  • C(3,0)=1
  • C(3,1)=3
  • C(3,2)=3
  • C(3,3)=1

但注意!上面例子中 3 1 2 4 → 16,而 3×1 + 1×3 + 2×3 + 4×1 = 3+3+6+4=16,完全吻合

解题策略

由于 N <= 12,全排列最多 12! 约等于 4.8 * 10^8,太大无法暴力枚举。

但我们可以使用 DFS + 剪枝 + 字典序优先搜索

  • 按字典序从小到大尝试每个位置填什么数字(1~N,不重复);
  • 在 DFS 过程中维护当前已选数字的加权和;
  • 如果当前和已经超过 sum,直接剪枝;
  • 一旦找到第一个合法解(因为按字典序搜索),立即返回(保证字典序最小)。

这正是代码所采用的方法。

代码解析

1. 预处理组合数系数 k[i]

tmp[0] = 1;
tmp[1] = 1;
for (int i = 2; i <= n; i++) tmp[i] = i * tmp[i - 1]; // 阶乘
for (int i = 0; i < n; i++)
    k[i] = tmp[n - 1] / tmp[n - 1 - i] / tmp[i]; // C(n-1, i)

计算每个位置的权重。

2. DFS 搜索(带状态压缩)

bool process(int flag, int index, int cur)

  • flag:位掩码,记录哪些数字已被使用(1-N对应bit:0-N-1);
  • index:当前要填第 index 个位置(0-indexed);
  • cur:当前加权和

递归终止条件:

  • index == n:所有位置填完,检查 cur == sum

剪枝:

  • cur > sum,提前返回 false(因为所有权重和数字都是正的,后续只会更大)。

尝试每个未使用的数字 i(1~N):

  • 设置 nums[index] = i
  • 递归调用 process(flag | (1<<(i-1)), index+1, cur + i * k[index])
  • 一旦某条路径成功,立即返回 true(保证字典序最小,因为 for 循环从小到大)。

3. 主函数:按字典序启动 DFS

for (int i = 1; i <= n; i++) {
    nums[0] = i;
    if (process(1 << (i - 1), 1, i * k[0])) {
        flag = true;
        break;
    }
}

从第一个位置依次尝试 1,2,...,N,一旦找到解就停止,确保字典序最小。

正确性与复杂度

  • 正确性:基于组合数加权模型 + 全排列搜索 + 剪枝。
  • 时间复杂度:最坏情况仍是 O(N!),但由于剪枝(cur > sum 提前退出)和 N <= 12,实际运行很快。
  • 空间复杂度:O(N),递归深度最多 12。

总结

本题的关键在于将多层相加过程转化为组合数加权和,从而将问题简化为带权重的排列搜索。通过 DFS + 状态压缩 + 剪枝 + 字典序优先,可以在合理时间内求解 N <= 12 的所有情况。

技巧总结

  • 杨辉三角系数 = 组合数 = 路径数 = 权重;
  • 小规模排列问题可用 DFS + 位掩码;
  • 字典序最小 ⇒ 从小到大枚举 + 找到即停;
  • 剪枝:当前和超过目标值可提前退出。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务