题解 P1118 【[USACO06FEB]数字三角形Backward Digit Su…】
Sweetlemon · · 题解
Upd: 这篇题解是我很久以前写的,当时错误也比较多;由于这篇题解的位置比较靠前,为防止误导,现在根据评论区的建议,对这篇题解进行更新。
拿到题目,是不是有一种“手足无措”的感觉呢?
在手足无措之前,先认真审题!
审题有两个要点:一是“
而且,并不是手足无措——我们可以暴力呢。
暴力的方法当然是,按字典序枚举最开始时的排列,对每一个排列都计算最后得到的数的值,如果这个值和题目给出的
如何按字典序枚举呢?可以使用 C++ 的 next_permutation 函数,也可以在 dfs 枚举排列时,每一层都从小到大枚举,这样自然就是按“字典序”枚举了。
这种方法有什么问题呢?当然是 跑得太慢了。于是我们要考虑优化“枚举”和“计算”的过程。
首先考虑优化“计算”过程。
要优化“计算”过程,就必须搞清楚这个数字三角形究竟是什么。
举例是理解的试金石。
——《数学女孩》
数学上来先打表。
—— OIer 行动准则(雾)
那我们就举例子吧!大家可以自己在草稿纸上写一下,假设
如果
如果
如果
观察各项的系数,你发现了什么?
如果你有敏锐的数学眼光,你会发现,各项系数恰与杨辉三角有关!恰好是杨辉三角中第
那么我们就可以快速计算出“某个排列玩游戏的结果”了!
注:如果你想要证明这个结论,可以考虑使用“数学归纳法”(如果不了解这个方法,可以查找相关资料),你会发现“相邻两项相加”和杨辉三角(也就是组合数)有密切的联系。不过在 OI 中证明倒显得不是特别重要。
下面是算法的过程。
首先,为了避免重复计算,我们算出杨辉三角的值——“预处理”。
由于我们要计算的是杨辉三角一行(第
边界是
上述公式可根据组合数的定义推导。我们还可以利用杨辉三角的对称性,只计算一半(当然算一半和算完并没有显著的效率差异,因为
当然,我们也可以用
甚至直接根据组合数的定义计算也是可以的,但是要注意避免溢出。
然后,使用深度优先搜索。因为 dfs 可以按照题目中的“字典序”依次得出答案,因此我们不需再进行判断。这里使用了一个全局数组 ans 来存储答案。
最后,如果有解,输出答案。这个比较简单,就略过了。
我们其实还可以进一步优化“枚举”的过程。考虑这个问题:如果一个排列前面的数的(带系数的)和就已经超过了
因此我们在枚举的过程中可以注意把已枚举出的数的(带系数的)和与
事实上,题目中的三档子任务,正对应了上面的三级优化!
下面是代码。
#include <cstdio>
using namespace std;
int n,sum;
//以下所有数组的大小都比所需值稍大,是为了防止越界
int visited[25]={0}; //防止重复选数,这是 dfs 枚举排列的要点
int ans[25]; //放置答案
int pc[25];//构造所有i C n-1
int dfs(int i,int num,int v); //写函数原型是(我的)好习惯!
int main(void){
scanf("%d%d",&n,&sum);
//下面构造杨辉三角(即组合数表)
pc[0]=pc[n-1]=1; //杨辉三角性质,两边都是1
if (n>1)
for (int i=1;i*2<n;i++)
pc[i]=pc[n-1-i]=(n-i)*pc[i-1]/i; //利用杨辉三角对称性和组合数公式计算
//下面枚举计算
if (dfs(0,0,0)) //0 仅起占位符作用
for (int i=1;i<=n;i++)
printf("%d ",ans[i]); //输出答案
return 0;
}
int dfs(int i,int num,int v){
//参数说明:i 表示已经枚举了前 i 个数(数的序号从 1 开始),num 表示第 i 个数是 num,v 表示前 i 个数的“和”为 v
//返回值说明:返回 0 表示不行(不可能),返回 1 表示找到了可行解。利用返回值就可以在找到第一个解后直接返回了
if (v>sum) //“剪枝”,及时排除不可能情况,加速枚举
return 0; //不可能
if (i==n){ //已经枚举了前 n 个(全部),判断一下是否是可行解
if (v==sum){
ans[i]=num; //放置解
return 1;
}
else
return 0;
}
visited[num]=1; //标记一下“第 i 个数的值已经使用过了”
//下面寻找第 i+1 个数
for (int j=1;j<=n;j++){
if (!visited[j] && dfs(i+1,j,v+pc[i]*j)){ //v+pc[i]*j表示前(i+1)个数的“和”
//注意,如果数的序号从 1 开始,那么第 i 个数的系数实际上是 (i-1) C (n-1)
//执行到这里表示已经找到了可行的解
ans[i]=num;
return 1;
}
}
visited[num]=0; //如果没有找到,一定记得复位,为进一步的寻找做准备
return 0; //执行到这里一定是没有找到解
}
Show you my code(C++,94 ms,776 KB).