【规律题】Backward Digit Sums

【POJ3187】【洛谷1118】Backward Digit Sums

Time Limit : 2000/1000ms (Java/Other)   Memory Limit : 131072/65536K (Java/Other)

Problem Description

FJ and his cows enjoy playing a mental game. They write down the numbers from 1 to N (1 <= N <= 10) in a certain order and then sum adjacent numbers to produce a new list with one fewer number. They repeat this until only a single number is left. For example, one instance of the game (when N=4) might go like this: 

    3   1   2   4

      4   3   6

        7   9

         16

Behind FJ's back, the cows have started playing a more difficult game, in which they try to determine the starting sequence from only the final total and the number N. Unfortunately, the game is a bit above FJ's mental arithmetic capabilities. 

Write a program to help FJ play the game and keep up with the cows.

Input

Line 1: Two space-separated integers: N and the final sum.

Output

Line 1: An ordering of the integers 1..N that leads to the given sum. If there are multiple solutions, choose the one that is lexicographically least, i.e., that puts smaller numbers first.

Sample Input

4 16

Sample Output

3 1 2 4

题意:

我们会发现,这个规律是个倒杨辉三角。

写出一个 1 至 N的排列 a_i,然后每次将相邻两个数相加,构成新的序列,再对新序列进行这样的操作,显然每次构成的序列都比上一次的序列长度少 1,直到只剩下一个数字位置。下面是一个例子:

3,1,2,4

4,3,6

7,9

16

最后得到 16 这样一个数字。

现在想要倒着玩这样一个游戏,如果知道 N,知道最后得到的数字的大小 sum,请你求出最初序列 a_i,为 1 至 N 的一个排列。若答案有多种可能,则输出字典序最小的那一个。

管理员注:本题描述有误,这里字典序指的是 1,2,3,4,5,6,7,8,9,10,11,12

而不是 1,10,11,12,2,3,4,5,6,7,8,9

分析:

思路

分析出来可以发现,题目给的要求的最后一位数字,实际上就是一个倒着的杨辉三角的权重分别乘以相应位置的数字后的和,可以通过STL里面的next_permutation按照字典序从小到大生成排列逐步生成数字序列,当得到第一个合乎要求的答案时输出此时的排列。

代码一:直接用next_permutation遍历(属于暴力了,HDU过了,洛谷TLE三个点) 

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> vec;
int tri[20];
int jc(int n)
{
	int sum=1;
	for(int i=1;i<=n;i++)
		sum*=i;
	return sum;
}
int count(int n,int k)
{
	n--,k--;
	return jc(n)/(jc(k)*jc(n-k));
}
void tri_arr(int n)
{
	for(int i=1;i<=n;i++)
		tri[i-1]=count(n,i);
}
int main()
{
	int m,ans,countans=0;
	cin>>m>>ans;
	tri_arr(m);
	for(int i=1;i<=m;i++)
		vec.push_back(i);
	do
	{
		for(int i=1;i<=m;i++)
			countans+=vec[i-1]*tri[i-1];
		if(countans==ans) 
		{
			cout<<vec[0];
			for(int i=1;i<m;i++)
				cout<<' '<<vec[i];
			cout<<endl;
			return 0;
		}
		countans=0;
	}while(next_permutation(vec.begin(),vec.end()));
	return 0;
}

代码二 优化版本(剪枝 跳过不可能排列段)

附惨痛的TLE

TLE四次之后某人突然想到,计算的半路上只要当前值大于目标值就不可能,可以直接跳过,高兴地拍桌子,改了之后,没过。。。。

然后又TLE了两次后想到。。。计算一个到一半就跳过这一个力度太小了,应该多跳几个,对哦,计算到一半就超过目标了,那后面爱咋排列咋排列与我有什么关系呢。于是根据next_permutation会从最小字典序到最大字典序的特性,我们直接让它从超过的位置为起始点直接进入最大字典序状态,下一次全排列就会跳过这些被判死刑了的排列。

        for(int i=1;i<=m;i++)
		{
            countans+=vec[i-1]*tri[i-1];
			if(countans>ans) 
			{
				sort(vec.begin()+i,vec.end(),cmp);
				flag=1;
				break;
			}
		}
		if(flag) continue;

通过sort实现部分字典序最大。

剪掉这部分枝之后就过了。emmmmmm

完整代码:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> vec;
int tri[12];
int jc(int n)
{
    int sum=1;
    for(int i=1;i<=n;i++)
        sum*=i;
    return sum;
}
int count(int n,int k)
{
    n--,k--;
    return jc(n)/(jc(k)*jc(n-k));
}
void tri_arr(int n)
{
    for(int i=1;i<=n;i++)
        tri[i-1]=count(n,i);
}
bool cmp(int a,int b){return a>b;}
int main()
{
    int m,ans,countans=0;
	bool flag=0;
    cin>>m>>ans;
    tri_arr(m);
    for(int i=1;i<=m;i++)
        vec.push_back(i);
    do
    {
		flag=0;countans=0;
        for(int i=1;i<=m;i++)
		{
            countans+=vec[i-1]*tri[i-1];
			if(countans>ans) 
			{
				sort(vec.begin()+i,vec.end(),cmp);
				flag=1;
				break;
			}
		}
		if(flag) continue;
        if(countans==ans) 
        {
            cout<<vec[0];
            for(int i=1;i<m;i++)
                cout<<' '<<vec[i];
            cout<<endl;
            return 0;
        }

    }while(next_permutation(vec.begin(),vec.end()));
    return 0;
}

【代码三 DFS版本】

这个题本来就是个搜索题,,DFS才是正解,时间复杂度比全排列暴力要低很多。

#include <stdio.h>
int n,sum;
int visited[13]={0}; //防止重复
int ans[13]; //放置答案
int pc[12];//构造所有i C n-1
int dfs(int,int,int); //写函数原型是好习惯!
int main(){
    int i;
    scanf("%d%d",&n,&sum);
    //下面开始构造杨辉三角(即组合数表)
    pc[0]=pc[n-1]=1; //杨辉三角性质,两边都是1
    if (n>1)
        for (i=1;i*2<n;i++)
            pc[i]=pc[n-1-i]=(n-i)*pc[i-1]/i; //利用杨辉三角对称性和组合数公式计算
    if (dfs(0,-1,0)) //-1仅起占位符作用
        for (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表示找到了可行解
    int j; //循环变量
    if (v>sum) //已经超了!
        return 0; //不可能
    if (i==n) //枚举到了最后一个,判断一下是否是可行解
        if (v==sum){
            ans[i]=num; //放置解
            return 1;
        }
        else
            return 0;
    visited[num]=1;
    for (j=1;j<=n;j++){
        if (!visited[j] && dfs(i+1,j,v+pc[i]*j)){//v+pc[i]*j表示前(i+1)个数的“和”
            //已经找到了可行的解
            ans[i]=num;
            return 1;
        }
    }
    visited[num]=0;//一定记得复位
    return 0;//执行到这里一定是没有找到解
}

补充:有关杨辉三角的知识

  1. 每个数等于它上方两数之和。
  2. 每行数字左右对称,由1开始逐渐变大。
  3. 第n行的数字有n项。
  4. 第n行数字和为2n-1。
  5. 第n行的m个数可表示为 C(n-1,m-1),即为从n-1个不同元素中取m-1个元素的组合数。
  6. 第n行的第m个数和第n-m+1个数相等 ,为组合数性质之一。
  7. 每个数字等于上一行的左右两个数字之和。可用此性质写出整个杨辉三角。即第n+1行的第i个数等于第n行的第i-1个数和第i个数之和,这也是组合数的性质之一。即 C(n+1,i)=C(n,i)+C(n,i-1)。
  8. (a+b)n的展开式中的各项系数依次对应杨辉三角的第(n+1)行中的每一项。
  9. 将第2n+1行第1个数,跟第2n+2行第3个数、第2n+3行第5个数……连成一线,这些数的和是第4n+1个斐波那契数;将第2n行第2个数(n>1),跟第2n-1行第4个数、第2n-2行第6个数……这些数之和是第4n-2个斐波那契数。
  10. 将各行数字相排列,可得11的n-1(n为行数)次方:1=11^0; 11=11^1; 121=11^2……当n>5时会不符合这一条性质,此时应把第n行的最右面的数字"1"放在个位,然后把左面的一个数字的个位对齐到十位... ...,以此类推,把空位用“0”补齐,然后把所有的数加起来,得到的数正好是11的n-1次方。以n=11为例,第十一行的数为:1,10,45,120,210,252,210,120,45,10,1,结果为 25937424601=1110。

猜你喜欢

转载自blog.csdn.net/u011590573/article/details/81220833