DP的本质是对问题划分状态空间进行遍历,然而有些无序性的排列问题条件限制较多,条件难以表达,这时可以尝试将无序性的排列问题转换为有序性的插入问题尝试简化问题,下面来看一下几道例题:
POJ2279 Mr Young’s Picture Permutations
描述: 有N个学生合影,站成左对齐的k排,每行分别有N1,N2…NK个人,第一排站最后,第k排站之前。
学生身高依次是1…N。在合影时候要求每一排从左到右递减,每一列从后面到前也递减,一共有多少总方案
输入
每组测试数据包含两行。第一行给出行数k。
第二行包含从后到前(n1,n2,…,nk)的行的长度,作为由单个空格分隔的十进制整数。
问题数据以0结束。
N<=30, k<=5;
输出 输出每组数据的方案数
样例输入
1
30
5
1 1 1 1 1
3
3 2 1
4
5 3 3 1
5
6 5 4 3 2
2
15 15
0
样例输出
1
1
16
4158
141892608
9694845
思路:一个典型的无序性排列问题,首先由于条件较多,每一行有人数限制且,每行每列要求顺序递增,所以我们尝试将问题变成有序性的插入问题,即从小到大插入,问题的状态有每行的人数,显然从小到大插入必须要插入到每行队伍的末端,1.若插入到中间的话,显然违反了单调 2.同时要保证插入后该行的人数必须要少于上一行的人数,因为如果不这样的话,上一行会留下空位且剩下的找不到更小的使其单调。
综上:以每一行拥有人数的方案数作为状态空间,设计动态规划的状态转移方程不一定要以如何计算出一个状态的形式给出,也可以考虑用一个已知的状态更新后续阶段的状态,要注意DP如果全开的话数组爆内存,所以dp[n][n/2][n/3][n/4][n/5] (这里我也不知道怎么看出来的)所以建议还是先输入了每行人数大小再定义数组
转移: 若要在第i行放一个人, 则需满足该行人数小于规定人数, 小于上一行人数(除了第一行)
起点: dp[0][0][0][0][0]=1
终点: dp[N1][N2][N3][N4][N5]
转移方程为:当a1<N1,f[a1+1,a2,a3,a4,a5]+=f[a1,a2,a3,a4,a5],其余同理。
代码如下
#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
int k,a[6];
int main()
{
while(scanf("%d",&k)&&k)
{
memset(a,0,sizeof(a));
for(int i=1;i<=k;++i)scanf("%d",&a[i]);
ll dp[a[1]+1][a[2]+1][a[3]+1][a[4]+1][a[5]+1];
memset(dp,0,sizeof(dp));
dp[0][0][0][0][0]=1;
for(int i=0;i<=a[1];++i)
for(int j=0;j<=a[2];++j)
for(int k=0;k<=a[3];++k)
for(int l=0;l<=a[4];++l)
for(int p=0;p<=a[5];++p)
{
if(i+1<=a[1])
dp[i+1][j][k][l][p]+=dp[i][j][k][l][p];
if(j+1<=a[2]&&j<i)
dp[i][j+1][k][l][p]+=dp[i][j][k][l][p];
if(k+1<=a[3]&&k<j)
dp[i][j][k+1][l][p]+=dp[i][j][k][l][p];
if(l+1<=a[4]&&l<k)
dp[i][j][k][l+1][p]+=dp[i][j][k][l][p];
if(p+1<=a[5]&&p<l)
dp[i][j][k][l][p+1]+=dp[i][j][k][l][p];
}
printf("%lld\n",dp[a[1]][a[2]][a[3]][a[4]][a[5]]);
}
return 0;
}
**
洛谷P2051 [AHOI2009]中国象棋
**
题目描述
这次小可可想解决的难题和中国象棋有关,在一个N行M列的棋盘上,让你放若干个炮(可以是0个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。大家肯定很清楚,在中国象棋中炮的行走方式是:一个炮攻击到另一个炮,当且仅当它们在同一行或同一列中,且它们之间恰好 有一个棋子。你也来和小可可一起锻炼一下思维吧!
思路:无序性排列问题,尝试有序插入,问题的状态有行数,列数,每行,每列的象棋的个数,所以尝试按行插入,这样一来问题就转化成功了,按行插入时,由于每行不能超过2个,状态转移时考虑前面列中象棋的个数,所以因此设出状态
f[i][j][k] 代表放了前ii行,有j列是有一个棋子,有k列是有2个棋子的合法方案数.
状态的种类数:没放棋子(empty)、空行、一列放了1个棋子,一列放了2个棋子,1列空棋子,
所以状态总数:没放棋子、放一个棋子在原有1个棋子上的、放1个棋子在没有放棋子的列上的、放两个棋子都在没有放棋子的列上、两个棋子都放在两个有1个棋子的列上、一个棋子在无棋子列且一个棋子放在有一个棋子的列上。
其中牵扯到一些组合计数的问题,但是请大家独立推出DP方程,知道状态种类数后真的不难
代码如下:
#include<cstdio>
#include<iostream>
#define maxn 110
#define mod 9999973
using namespace std;
long long f[maxn][maxn][maxn];
long long n,m;
int main()
{
cin>>n>>m;
f[0][0][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
int MAX_K=m-j;
for(int k=0;k<=MAX_K;k++)
{
f[i][j][k]=f[i-1][j][k];//一个棋子也不放的情况
if(j>=1) f[i][j][k]=(f[i][j][k]+f[i-1][j-1][k]*(m-k-j+1))%mod;//放置1个棋子,并且此棋子放置在原来有0个棋子的一列上
if(k>=1) f[i][j][k]=(f[i][j][k]+f[i-1][j+1][k-1]*(j+1))%mod;//放置1个棋子,并且此棋子放置在原来有1个棋子的一列上
if(j>=2) f[i][j][k]=(f[i][j][k]+f[i-1][j-2][k]*(((m-j-k+1)*(m-j-k+2))/2))%mod;//放置两个棋子,并且两个棋子都放置在原来有0个棋子的两列上
if(j&&k) f[i][j][k]=(f[i][j][k]+f[i-1][j][k-1]*(j*(m-j-k+1)))%mod;//放置两个棋子,并且两个棋子分别放置在原来有0个棋子的一列和原来有1个棋子的一列上
if(k>=2) f[i][j][k]=(f[i][j][k]+f[i-1][j+2][k-2]*(((j+1)*(j+2))/2))%mod;//放置两个棋子,并且两个棋子都在原来有1个棋子的两列上
}
}
}
long long ans=0;
for(int i=0;i<=m;i++)
{
int max_n=m-i;
for(int j=0;j<=max_n;j++)
{
ans=(ans+f[n][i][j])%mod;
}
}
cout<<ans<<endl;
return 0;
}
综上遇到无序性排列问题时,可以考虑变为有序性插入问题,简化问题条件,设计容易递推的状态,两种想法
设计动态规划的状态转移方程不一定要以如何计算出一个状态的形式给出,也可以考虑用一个已知的状态更新后续阶段的状态
哪种好想选哪种