题意:题目首先告诉我们神马叫做A Unimodal Palindromic sequence,也就是一个序列先递增然后再递减(即单峰),而且是对称的。
已知一个数字N,我们可以把数字N拆分成很多个Unimodal Palindromic sequence,例如
1: (1)
2: (2), (1 1)
3: (3), (1 1 1)
4: (4), (1 2 1), (2 2), (1 1 1 1)
5: (5), (1 3 1), (1 1 1 1 1)
6: (6), (1 4 1), (2 2 2), (1 1 2 1 1), (3 3),
(1 2 2 1), ( 1 1 1 1 1 1)
7: (7), (1 5 1), (2 3 2), (1 1 3 1 1), (1 1 1 1 1 1 1)
8: (8), (1 6 1), (2 4 2), (1 1 4 1 1), (1 2 2 2 1),
(1 1 1 2 1 1 1), ( 4 4), (1 3 3 1), (2 2 2 2),
(1 1 2 2 1 1), (1 1 1 1 1 1 1 1)
现在题目要我们求数字n有多少个Unimodal Palindromic sequence。
题解:
看到题目,我们很容易想起整数划分问题,现在我们先回忆一下整数划分问题的解法吧
整数划分问题
将一个整数分成几部分,使得这些部分的和为该整数。
给例子吧
1:1 (共1个)
2:2 1+1 (共2个)
3:3 1+2 1+1+1 (共3个)
4:4 1+3 2+2 1+1+2 1+1+1+1 (共5个)
5:5 1+4 2+3 1+2+2 1+1+3 1+1+1+1+1 (共6个)
直接给定一个数字n,要我们求该整数划分的个数,直接求貌似不好求,我们可以构造递归式
f(n,m)表示将整数n划分后的每部分都不能大于m的划分数目,得到该表达式,我们就可以就行转化了
1、当n<m时,很显然f(n,m)=f(n,n)
2、当n==m时,f(n,m)=f(n,m-1)+1,即分成划分数里有和没有数字m的两种情况
3、当n>m时,在n的划分中,如果没有大于m的数的划分总数为f(n,m-1);划分中有m的划分总数为f(n-m,m),
f(n,m)=f(n,m-1)+f(n-m,m);
递归和非递归代码如下
#include <stdio.h>
#include <string.h>
#define N 1000
long long dp[N][N];
// 递归解法
long long divide(int n, int m)
{
if (n == 0)
return 1;
if (m == 0)
return 0;
if (n < m)
return divide(n, n);
else
return divide(n, m-1) + divide(n-m, m);
}
int main(void)
{
int i;
int j;
int n;
while (scanf("%d", &n) && n)
{
//非递归解法
dp[0][0] = 1;
for (i = 1; i <= n; i++)
{
dp[i][1] = 1;
for (j = 2; j <= i; j++)
{
if (i >= 2 * j)
dp[i][j] = dp[i][j-1] + dp[i-j][j];
else
dp[i][j] = dp[i][j-1] + dp[i-j][i-j];
}
}
printf("%d %lld\n", n, dp[n][n]);
printf("%d %lld\n", n, divide(n, n));
}
return 0;
}
回到正题,我们可以发现题目要求的序列数其实是整数划分的子部分,即具有对称性的子划分列数
发现了这个以后,我们就可以将其转化为整数划分的解法
解法一、整数划分
1、假设n为奇数,我们可以发现,为了使得划分的和为奇数,划分的元素的个数肯定为奇数个,并且关于这个奇数对称,我们可以枚举这个最中间的奇数s(1<=s<=n),剩下的部分n-s就是对称的两部分,所以我们只需求整数(n-s)/2的划分数f((n-s)/2)。
2、假设n为偶数,若将n划分为奇数部分,为了使得划分的总和为偶数,最中间的那个数s(2<=s<=n)必须是偶数,其实此时和n为奇数的情况一致了,只是枚举s的时候,我们是枚举所有小于n的偶数;若划分为偶数部分,我们直接应用整数划分的方法f(n/2,n/2)。
这样一分情况,解法也很了然了
代码如下
#include <stdio.h>
#include <string.h>
#define N 1000
typedef long long ll;
ll dp[N][N];
int main(void)
{
int i;
int j;
int n;
for (i = 0; i < 400; i++)
{
dp[i][0] = 1;
dp[i][1] = 1;
for (j = 2; j < 400; j++)
{
if (j > i)
dp[i][j] = dp[i][i];
else if (i == j)
dp[i][j] = dp[i][j-1] + 1;
else
dp[i][j] = dp[i][j-1] + dp[i-j][j];
}
}
while (scanf("%d", &n) && n)
{
int k;
ll ans = 0;
if (n & 1)
{
for (k = 1; k <= n; k += 2)
ans += dp[(n-k)/2][k];
}
else
{
ans += dp[n/2][n/2];
for (k = 2; k <= n; k += 2)
ans += dp[(n-k)/2][k];
}
printf("%d %lld\n", n, ans);
}
return 0;
}
解法二、动态规划1
其实解法一也是动态规划解法,这里介绍另外一种动态规划解法。(不是自己想到的,看discuss里人说的)
dp[i][j]表示将整数i划分的每部分都不小于j的划分个数。
1、假设最左边和最右边的两个数正好是j,则有划分数dp[i-2j][j]
2、假设最左边和最右边的两个数大于j,则有划分数dp[i][j+1]
综上:dp[i][j] = dp[i-2j][j] + dp[i][j+1]
代码如下
#include <stdio.h>
#include <string.h>
#define N 1000
long long dp[N][N];
int main(void)
{
int i;
int j;
int n;
while (scanf("%d", &n) && n)
{
for (i = 0; i <= n; i++)
dp[0][i] = 1;
for (i = 1; i <= n; i++)
{
dp[i][i] = 1;
for (j = i-1; j > 0; j--)
{
if (i >= 2 * j)
dp[i][j] = dp[i-2*j][j] + dp[i][j+1];
else
dp[i][j] = dp[i][j+1];
}
}
printf("%d %lld\n", n, dp[n][1]);
}
return 0;
}
解法三、动态规划2
这里说下另外一种动态规划的解法(discuss里说的,o(︶︿︶)o 唉,自己咋就一个都想不出来呢)
dp[i][j]表示将i划分成j部分的个数。
1、假设最左边和最右边的数都为1,则我们去掉这两个数,划分数变成了dp[i-2][j-2]
2、假设最左边和最右边的数都大于1,则我们可以将所有数都减去1,划分数变成了dp[i-j][j]
综上,dp[i][j] = dp[i-2][j-2] + dp[i-j][j]
最后要求总的划分数,枚举j(1<=j<=n)求和即可
代码如下
#include <stdio.h>
#include <string.h>
#define N 400
typedef long long ll;
ll dp[N][N];
int main(void)
{
int i;
int j;
int n;
dp[0][0] = 1;
for (i = 1; i < N; i++)
{
dp[i][0] = 0;
dp[i][1] = 1;
dp[i][i] = 1;
for (j = 2; j < i; j++)
dp[i][j] = dp[i-2][j-2] + dp[i-j][j];
}
while (scanf("%d", &n) && n)
{
ll ans = 0;
for (i = 1; i <= n; i++)
ans += dp[n][i];
printf("%d %lld\n", n, ans);
}
return 0;
}