[AHOI2009] 中国象棋 解题报告(动态规划)

题目传送门

神秘传送门

题目描述

    这次小可可想解决的难题和中国象棋有关,在一个N行M列的棋盘上,让你放若干个炮(可以是0个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。大家肯定很清楚,在中国象棋中炮的行走方式是:一个炮攻击到另一个炮,当且仅当它们在同一行或同一列中,且它们之间恰好 有一个棋子。你也来和小可可一起锻炼一下思维吧!

输入输出格式

输入格式:

    一行包含两个整数N,M,之间由一个空格隔开。

输出格式:

    总共的方案数,由于该值可能很大,只需给出方案数模9999973的结果。

输入输出样例

输入样例#1: 
1 3
输出样例#1: 
7



说明

样例说明

    除了3个格子里都塞满了炮以外,其它方案都是可行的,所以一共有2*2*2-1=7种方案。

数据范围

    100%的数据中N和M均不超过100

    50%的数据中N和M至少有一个数不超过8

    30%的数据中N和M均不超过6


解题报告

    很显然,若要炮不能互相攻击,那么每行每列的炮不能超过2个。

    动态规划的话,可以用DP[i][k]表示前i行状态为k(k为一个三进制数,表示每列放了多少个)的方案数,转移的话只要枚举每行放哪一个或哪两个位置放就行了,这样的话时间复杂度为O(m^2*n*3^m),肯定过不了,原因就是状态太复杂。

    那怎么优化状态呢?

    可以发现我们其实不需要知道具体每一列的状态,只要知道个数为0/1/2的各有几列就行了,因为只要0/1/2的列数相同,那么这些状态虽然排布不同,但本质上是一样的,直接乘法就可以得到。(另一种理解就是每次放完之后把所有炮个数相同的列都移到一起,例如把02101变成00112本质上是一样的)

    这样,我们就可以用Opt[i][j][k]表示前i行有j列不放有k列放1个的方案数(放两个的就是m-k-j)。

    现在来考虑状态的转移:

        Opt[i][j][k]+=Opt[i-1][j][k]; //不放的情况

            不放的话状态不变,所以也没有限制条件。

        Opt[i][j][k]+=Opt[i-1][j+1][k-1]*(j+1); //放一个在原来没有的上

            原来没有的少了一列,1个的多了一列,原来没有的都可以放(即(j+1)种放法),要满足当前1个的至少一列(即k>0)

        Opt[i][j][k]+=Opt[i-1][j][k+1]*(k+1); //放一个在原来有1个的上

            原来1个的少了一列,2个的多了一列,原来1个的都可以放(即(k+1)种放法)要满足当前2个的至少一列(即m-k-j>0) 

        Opt[i][j][k]+=Opt[i-1][j+1][k]*(j+1)*k; //一个放在原来没有的,另一个放在原来有一个的

            原来没有的少了一列,1个的多了一列又少了1列,有(j+1)*k种放法,要满足k>0&&(m-k-j>0)

        Opt[i][j][k]+=Opt[i-1][j+2][k-2]*(j+2)*(j+1)/2; //两个都放在原来没有的上

            原来没有的少了两列,1个的多了两列,有(j+1)*(j+2)种放法,要满足当前1个的至少两列(即k>1)

        Opt[i][j][k]+=Opt[i-1][j][k+2]*(k+2)*(k+1)/2; //两个都放在原来有一个的上

            基本同上。

    初始化时一定是Opt[0][m][0]=1!!!

源代码

#include<bits/stdc++.h>
using namespace std;
int n,m,i,j,k,Ans=0;
long long Opt[101][300][300]; 	//Opt[i][j][k]表示前i行有j列没放有k列放了1个的方案数 
int main(){
	cin>>n>>m;
	Opt[0][m][0]=1;				//初始化时一个都不放的情况赋值为1(不能误写成Opt[0][0][0]) 
	for(i=1;i<=n;++i)
		for(j=0;j<=m;++j)
			for(k=0;k<=m-j;++k){
				Opt[i][j][k]+=Opt[i-1][j][k];				//不放的情况 
				if(k)			
					Opt[i][j][k]+=Opt[i-1][j+1][k-1]*(j+1);		//放一个在原来没有的上 
				if(m-k-j)		
					Opt[i][j][k]+=Opt[i-1][j][k+1]*(k+1);		//放一个在原来有1个的上 
				if(k&&(m-k-j))	
					Opt[i][j][k]+=Opt[i-1][j+1][k]*(j+1)*k;		//一个放在原来没有的,另一个放在原来有一个的 
				if(k>=2)		
					Opt[i][j][k]+=Opt[i-1][j+2][k-2]*(j+2)*(j+1)/2;	//两个都放在原来没有的上 
				if(m-j-k>=2)			
					Opt[i][j][k]+=Opt[i-1][j][k+2]*(k+2)*(k+1)/2;	//两个都放在原来有一个的上 
				Opt[i][j][k]%=9999973;
			}
	for(j=0;j<=m;++j)
		for(k=0;k<=m-j;++k)
			Ans=(Ans+Opt[n][j][k])%9999973;
	cout<<Ans<<endl;
}

解题小结

    动态规划问题中,若遇到状态过于复杂的情况,要考虑状态的简化,尽量将同一类的状态合并,从而简化计算。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值