题解:洛谷 P1118 Backward Digit Sums

【题目来源】

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

【题目描述】

FJ 和他的奶牛们喜欢玩一个心算游戏。他们将数字从 \(1\)\(N(1 \le N \le 12)\) 按某种顺序写下来,然后将相邻的数字相加,得到一个数字更少的新列表。他们重复这个过程,直到只剩下一个数字。例如,游戏的一种情况(当 \(N=4\) 时)可能是这样的:

    3   1   2   4
      4   3   6
        7   9
         16

FJ 背后,奶牛们开始玩一个更难的游戏,她们试图从最终的总和和数字 \(N\) 中确定起始序列。不幸的是,这个游戏有点超出了 FJ 的心算能力。

编写一个程序来帮助 FJ 玩这个游戏,并跟上奶牛们的步伐。

【输入】

共一行两个正整数 \(n,sum\)

【输出】

输出包括一行,为字典序最小的那个答案。

当无解的时候,请什么也不输出。

【输入样例】

4 16

【输出样例】

3 1 2 4

【核心思想】

  1. 问题分析:给定 \(N\)\(sum\),需要找到一个 \(1\)\(N\) 的排列,使得按照"相邻数字逐层相加"规则(类似杨辉三角的逐层求和)最终得到的数字等于 \(sum\)。例如 \(N=4\) 时,排列 \((3,1,2,4)\) 的逐层求和过程为:

    3   1   2   4
      4   3   6
        7   9
         16
    

    这是一个DFS全排列搜索 + 剪枝优化问题,关键在于利用逐层求和过程中 \(b[1]\) 单调递增的特性进行提前剪枝。

  2. 算法选择

    • DFS全排列枚举:用 book 数组标记已使用的数字,递归生成 \(1\)\(N\) 的所有排列
    • 逐层求和验证calc() 函数模拟题目描述的相邻相加过程,计算最终值
    • 前缀剪枝:在 calc() 中,若某层求和后 \(b[1] > sum\),立即返回 \(-1\),避免无效搜索
  3. 关键步骤

    • DFS搜索(当前步数 \(step\)):
      • 剪枝验证:调用 calc() 计算当前部分排列的逐层和,若返回 \(-1\)(已超 \(sum\))则直接返回
      • 终止条件:若 \(step > N\)calc() == sum,输出当前排列并结束程序
      • 枚举尝试:遍历 \(i\)\(1\)\(N\),若 \(book[i] = 0\)
        • \(a[step] = i\)\(book[i] = 1\)
        • 递归 \(dfs(step+1)\)
        • 回溯:\(a[step] = 0\)\(book[i] = 0\)
    • 逐层求和计算calc()):
      • \(a\) 拷贝到 \(c\)
      • 进行 \(N-1\) 轮相邻相加:\(b[j] = c[j] + c[j+1]\)
      • 每轮若 \(b[1] > sum\) 返回 \(-1\)
      • \(b\) 拷贝回 \(c\) 继续下一轮
      • 返回最终 \(c[1]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N! \cdot N^2)\),最坏枚举 \(N!\) 个排列,每个排列验证 \(O(N^2)\)
    • 空间复杂度:\(O(N)\),排列数组、标记数组及临时数组
  5. DFS剪枝的核心思想

    • 杨辉三角系数:最终和 \(= \sum_{i=1}^{N} a_i \times C(N-1, i-1)\),即每个位置 \(i\) 的贡献系数为组合数。但代码采用直接模拟,更直观
    • 前缀单调性剪枝:逐层求和过程中,\(b[1]\)\(a\) 中前若干元素的加权和,随着排列增长单调不减(对于正数),一旦超过 \(sum\) 可立即剪枝
    • 字典序最小保证:DFS按 \(1\)\(N\) 的顺序枚举,首次找到的合法解即为字典序最小解
    • 全排列模板:标准的 book 标记 + 回溯框架,适用于 \(N \le 12\) 的小规模排列搜索
    • 适用于"排列约束满足 + 可验证目标值"类问题,核心是利用目标函数的单调性进行有效剪枝

【解题思路】

【算法标签】

普及 #DFS-一维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int n, sum, a[15], b[15], c[15], book[15];
int calc()  // 按照游戏规则计算
{
    for (int i=1; i<=n; i++) {  // 将a数组拷贝至c数组
        c[i] = a[i];
    }
    for (int i=1; i<n; i++) {  // 依次遍历c数组中所有数
        for (int j=1; j<n; j++) {  // 相邻两个数相加,并赋值给b数组
            b[j] = c[j]+c[j+1];
        }
        if (b[1]>sum) {  // 如果计算后b[1]已经大于sum,则无需继续计算,返回-1
            return -1;
        }
        for (int j=1; j<=n; j++) {  // 将b数组拷贝至c数组,进行下一轮计算
            c[j] = b[j];
        }
    }
    return c[1];  // 返回c数组第1个元素的值
}
void dfs(int step)
{
    int tmp = calc();  // 剪枝,每次都计算一下
    if (tmp==-1) return;  // 如果c[1]已经超过sum,后面就不用算了
    if (step>n) {  // 搜索退出条件
        if (tmp==sum) {  // 如果等于sum
            for (int i=1; i<=n; i++) {  // 则输出a数组
                cout << a[i] << " ";
            }
            cout << endl;
            exit(0);  // 并退出程序
        }
        return;  // 如果不等于,即小于,还要继续搜索
    }
    for (int i=1; i<=n; i++) {  // 全排列模板
        if (book[i]==0) {  // 如果某个数没有被用过
            a[step]=i;  // 就用这个数
            book[i]=1;  // 并标记用过
            dfs(step+1);  // 进行下一次搜索
            a[step]=0;  // 还原现场
            book[i]=0;
        }
    }
}
int main()
{
    cin >> n >> sum;  // 输入n和sum
    dfs(1);  // 进行dfs深搜
    return 0;
}

【运行结果】

4 16
3 1 2 4
posted @ 2026-08-27 11:06  团爸讲算法  阅读(9)  评论(0)    收藏  举报