poj1221 dp

题意:题目首先告诉我们神马叫做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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值