先简单说一下题意:给定一个只包含‘I’、‘D’、‘?’的字符串,其长度记为len,求解满足这种由字符串规定变化规律的(len+1)的排列个数,‘I’(increasing)表示前一个数字比后一个数字大,‘D’(decreasing)表示前一个数字比后一个数字小,‘?’表示前一个数字与后一个数字的大小关系任意。比如:满足“DIIDID”变化规律的一个排列是 3 1 2 7 4 6 5,即减小、增大、增大、减小、增大、减小的变换关系,题目链接:点击打开链接
这里还有一个官方的hint:
Permutation {1,2,3} has signature "II".Permutations {1,3,2} and {2,3,1} have signature "ID".Permutations {3,1,2} and {2,1,3} have signature "DI".Permutation {3,2,1} has signature "DD"."?D" can be either "ID" or "DD"."??" gives all possible permutations of length 3.
HDU4055 乍一看不像DP问题,很明显,确实不具有最优子结构,你想啊,容易想到把状态定义为F(i)(1<=i<=n),表示i的符合条件的排列,但是i的具体的排列对后面明显有影响呢?
而后数十分钟的苦苦思索,却不得其所,为此求解各大博主,并测试了好几个博主的效率和方法,弄了我这个比较好懂且效率还比较可以的算法,题目要求java10s以内,其他语言5s以内,本算法大约1.7s,测试过几个3s和4s的,并且感觉写得太复杂。
下面介绍计数dp经典算法:
状态一开始就没有找对,怎么能那么直接的定义状态呢,一般是很难直接求解的,在这里定义dp[i][j]为状态,表示i的满足变换规律的排列,并且以数字j为结尾。
边界条件很简单:dp[1][1] = 1,但是注意每个测试样例必须要全部初始化为0,因为在计算过程中会需要一些并没有计算的值,这些值为0,正好避免了设置条件判断,在后面的代码中有提示的地方。
状态转移方程为:(sum表示求和)
简单理解去掉后效性:处理dp[1~i][]的过程中i是依次1~n相加。处理完dp[i-1][]后,加入的数即为i,而dp[i][j]是要将i放进去j换 出来,而这里有一种将i放进去j换出来,同时不影响升降顺序的方法是:将dp[i-1][j]的i-1个数的序列中 ≥j 的数都加1,这样i-1变成了i,j变成了j+1,而j自然就补在后面了。 --来自某位机智的博主(点击打开链接)(感谢提点)
1.‘I’:dp[i][j] = sum(dp[i-1][x])(1<=x<=j-1) 这个过程可以化简为:dp[i][j] = dp[i][j-1] + dp[i-1][j-1];
2.‘D’:dp[i][j] = sum(dp[i-1][x])(j<=x<=i-1) 可化简为:dp[i][j] = dp[i][j+1] +dp[i-1][j];
3.‘?’:dp[i][j] = sum(dp[i-1][x])(1<=x<=i-1)
这个过程还是比较好理解的,只要搞懂前面的怎么让这个过程变成无后效性。
具体代码如下:
#include<iostream>
#include<string>
using namespace std;
const int m = 1e9 + 7;
const int n = 1010;
int dp[n][n];
string s;
int main()
{
while (cin >> s)
{
int len = s.length()+1;//排列的最大数比字符个数大1
memset(dp, 0, sizeof(dp));
dp[1][1] = 1;
for (int i = 2; i <= len; ++i)
{
char jud = s[i - 2];
if (jud == 'I')
{
for (int j = 2; j <= i; ++j)//没有1结尾的递增,当然也可以写进去
{
dp[i][j] = (dp[i][j - 1] + dp[i - 1][j - 1]) % m;
}
}
else if (jud == 'D')
{
for (int j = i-1; j >=1; --j)
{
dp[i][j] = (dp[i][j + 1] + dp[i - 1][j]) % m;
}
}
else
{
int sum = 0;
for (int j = 1; j < i; ++j)
{
sum = (sum + dp[i - 1][j]) % m;
}
for (int j = 1; j <= i; ++j)
dp[i][j] = sum;
}
}
int sum = 0;
//计算所有符合条件的排列
for (int j = 1; j <= len; ++j)
sum = (sum + dp[len][j]) % m;
printf("%d\n", sum);
}
return 0;
}