【蓝桥杯】历届试题 格子刷油漆(动态规划)

历届试题 格子刷油漆

问题描述

X 国的一段古城墙的顶端可以看成 2 × N 2\times N 2×N 个格子组成的矩形(如下图所示),现需要把这些格子刷上保护漆。
例如下图是一个长度为 3,高为 2 的城墙:
n=3的城墙
你可以从任意一个格子刷起,刷完一格,可以移动到和它相邻的格子(对角相邻也算数),但不能移动到较远的格子(因为油漆未干不能踩!)。比如:
a d b c e f 就是合格的刷漆顺序。
c e f d a b 是另一种合适的方案。
当已知 N 时,求总的方案数。当 n n n 较大时,结果会迅速增大,请把结果对 1000000007 (十亿零七) 取模。

输入格式

输入数据为一个正整数(不大于 1000)

输出格式

输出数据为一个正整数。

样例输入 1

2

样例输出 1

24

样例输入 2

3

样例输出 2

96

样例输入 3

22

样例输出 3

359635897



—— 轻舟已过万重山 ——


本题的实质是在已知几个基本行走规则的前提下,求解遍历整个矩形的行走路线数量。

很多同学的第一想法是搜索,但在本题 n n n 最大可取到 1000 的前提下,搜索必然超时(递归层次太深),因此我们不得不另辟蹊径。实际上,这些搜索可行,但又不可解的题大多数都是在考察动态规划,本题就是一个很好的例子!不过从题目的描述来看,这道题也更像是一道动规的题目(类似题目:动态规划之走格子问题,即给你行走规则,让你输出总的方案数)。

对于题目给出的几类行走规则,我们可以很容易地联想到递推。因为对于某个格子,其走到当前可能有很多种走法,但是从另一个角度看来,以某个格子为出发点进行 “刷漆”,其行走方式却是固定的。我们首先要做的,就是来分析这个过程,并找到动态转移方程。对城墙刷漆时,我们的起点主要分为以下两大类:

  1. 从四个顶点之一出发。
  2. 从中间某个点出发。

下面逐个分析。



1、从四个顶点出发


在这前提下,又可细分为以下 3 类行走方式:

  • 第一步走同一列的另一个格子,第二步走下一列的任意格子……如此循环直到走完所有列。
  • 第一步走下一列的任意格子,第二步再走下一列的任意格子……如此行走直到最后一列。返回时路径唯一。
  • 第一步走下一列的任意格子,第二步由该格子返回前一列剩下的尚未走的格子,第三步再来到前一步所在列剩下的尚未走的格子……如此循环直到走完所有列。

下面对这些情况进行进一步的描述与分析。

  1. 第一步走同一列的另一个格子,第二步走下一列的任意格子……如此循环直到走完所有列。这个过程如下图所示:
    在这里插入图片描述
    现假设初始情况下从顶点 A 出发。根据描述,第一步应该走向 B。接下来在 B 点时就有两种选择方案:要么走向 C;要么走向 D。而无论选择走哪一点,接下来的情形都将回到初始情况:“从某个顶点出发,走完一个 m × 2 m\times 2 m×2 的格子(这里的 m m m 自然取 n − 1 n-1 n1)”。
    如果我们将这种 “一趟过去,不再返回” 的行走方式用数组 a [   ] a[\ ] a[ ] 来表示,则从上面的分析可知,它存在一个递推关系,即:
    a [ i ] = 2 ∗ a [ i − 1 ] a[i] = 2*a[i-1] a[i]=2a[i1]
    上式中,乘 2 的原因是每次往下一列走时都有两种选择,如在 B 点时可选择 C 或 D 作为接下来的落点。

  1. 第一步走下一列的任意格子,第二步再走下一列的任意格子……如此行走直到最后一列。返回时路径唯一。这个过程如下图所示(红色箭头表示去的行程路径,蓝色箭头表示回的行程路径):
    在这里插入图片描述
    现假设初始情况下从顶点 A 出发。根据描述,第一步可以选 C或 D作为接下来的落点(假设我们选的是 C);那么接下来又可以选择 E 或 F 作为第二步的落点(假设我们选的是 E)……当走到最后一列(假设最后一列的落点为 I),此时返回的路线也被唯一确定了,如下图所示(J→G→F→D→B):
    在这里插入图片描述
    如果我们将这种 “一趟过去,一趟回来” 的行走方式用数组 b [   ] b[\ ] b[ ] 来表示,则从上面的分析可知,它存在一个递推关系,即:
    b [ i ] = 2 ∗ b [ i − 1 ] b[i] = 2*b[i-1] b[i]=2b[i1]
    上式中,乘 2 的原因是每次往下一列走时都有两种选择,如在 A 点可选 C 或 D。

  1. 第一步走下一列的任意格子,第二步由该格子返回前一列剩下的尚未走的格子,第三步再来到前一步所在列剩下的尚未走的格子……如此循环直到走完所有列。这个过程如下图所示:
    在这里插入图片描述
    现假设初始情况下从顶点 A 出发。根据描述,第一步有两条路线:A→C→B→D(或 A→D→B→C),假设最终落点为 D,则在 D 时将面临两种选择,要么到 E 要么到 F),而无论选择哪一点,接下来都将回到初始情况:“从某个顶点出发,走完一个 m × 2 m\times 2 m×2 的格子(这里的 m m m 自然取 n − 2 n-2 n2)”。
    该走法也属于 “一趟过去,不再返回” 的类型,因此这里依然用数组 a [   ] a[\ ] a[ ]来表示。但是注意到一点:这种行走方式从一种状态到下一种状态需要的最小行走次数为 2,因此其状态转移方程为:
    a [ i ] = 2 ∗ 2 ∗ a [ i − 2 ] = 4 ∗ a [ i − 2 ] a[i] = 2*2*a[i-2]=4*a[i-2] a[i]=22a[i2]=4a[i2]
    上式中,第一个乘 2 是选下一列的落点 C 或 D,第二个乘 2 是选再下一列的落点 E 或 F。

以上便是从顶点出发刷完所有墙面的全部行走方式。但需要注意的是,在实际刷墙时,这些行走方式可以任意组合,而不仅仅是单一化的。比如,可以先用方法 1 走一段,再用方法 2 走另一段;或者先用方法 3 走一段,再用方法 1 走一段,再用方法 2 走最后一段……这些组合方式是不胜枚举的。因此在进行状态转移的时候,我们需要将上面这三种情况都加到一起。在这样的前提下,我们可以直接将 a [   ] a[\ ] a[ ] 数组的含义赋予为 “从墙壁顶点处出发,刷完整面墙的方案数”,于是可以得到新的状态转移方程为:

a [ i ] = 2 ∗ a [ i − 1 ] + b [ i ] + 4 ∗ a [ i − 2 ] a[i] = 2*a[i-1] + b[i] + 4*a[i-2] a[i]=2a[i1]+b[i]+4a[i2]

对于 “一趟过去,一趟返回” 的走法而言,他是独特的,因为他的遍历路径不能插入其他任何遍历方式:其只有两趟,一趟必须到底,另一趟则走剩余路径。因此他的走法在迭代时,与其他走法(方法 1、方法 3)没有任何关系,而仅仅取决于其前一种状态。所以他的动态转移方程为:

b [ i ] = 2 ∗ b [ i − 1 ] b[i] = 2*b[i-1] b[i]=2b[i1]

注:由于从顶点出发时有 4 个顶点,因此最后的结果实际上是 4 ∗ a [ n ] 4*a[n] 4a[n]



2、从中间出发


由于墙的高度为 2 且不能走已经刷过的点,因此从中间某个点出发时必然是先刷该点的某一边,然后再倒回来刷该点的另一边。见下图,假设我们从图中 i = 3 i=3 i=3(E点)处出发(以 i = 3 i=3 i=3 为分割线,将图分为左边的 ABCDEF 以及右边的 GHIJ),为了遍历所有格子,我们必须先走完左边的 ABCDEF 才能继续走右边的 GHIJ(注意:出发点为 E 时,回来的终点必须是F),然后再把右边的 GHIJ 视为以 G(或 H)为起点的一组格子,并将其走完(因此这里有两个出发点,所以需要乘以2)。

在这里插入图片描述

在上面的遍历过程中,当从 E 点出发并往左边走时,由于其必须返回到 i = 3 i=3 i=3 列的 F 点,因此属于 “一趟过去,一趟回来” 的类型。在左边长度为 i = 3 i=3 i=3 时,左边的遍历方式就有 b [ i ] b[i] b[i] 种;当从 F 出发时,由于其有两个落点(G 或 H),因此这里要乘以 2。接下来,无论在哪一个点出发,此时既可以选择 “一趟过去,一趟回来”,也可以选择 “一趟过去,不再返回”,还可以选择两者的任意合法组合。总之,就是前面我们已经修改后的 a [   ] a[\ ] a[ ] 数组( “从墙壁顶点处出发,刷完整面墙的方案数” )。于是,可以得到整个右边的行走方式有 2 ∗ a [ n − i ] 2*a[n-i] 2a[ni] 种。于是,可以得到从 E 点出发先往左侧移动时,总的刷漆方式有 b [ i ] ∗ 2 ∗ a [ n − i ] b[i] * 2*a[n-i] b[i]2a[ni] 种。此外,一开始时,我们还可以选择从 F 点出发,然后回到 E 点,所以这里还需要再乘以 2,即:

2 ∗ b [ i ] ∗ 2 ∗ a [ n − i ] = 4 ∗ b [ i ] ∗ a [ n − i ] 2*b[i]*2*a[n-i] = 4*b[i]* a[n-i] 2b[i]2a[ni]=4b[i]a[ni]

还有一种情况,当我们从 E 出发时先往右边 EFGHIJ 走(最终返回至 F 点),这时,该部分的遍历方案就有 b [ n − i + 1 ] b[n-i+1] b[ni+1] 种;接下来对于左边的 ABCD,同样是 “从墙壁顶点处出发,刷完整面墙的方案数” 这一方式,即 2 ∗ a [ i − 1 ] 2*a[i-1] 2a[i1] 种(这里乘以 2 依然是因为在 ABCD 部分,其出发点可以选择 C 或者 D)。于是,可得到总的刷漆方式为 b [ n − i + 1 ] ∗ 2 ∗ a [ i − 1 ] b[n-i+1]*2*a[i-1] b[ni+1]2a[i1] 种。同样地,这里一开始的出发点可以选 E 也可以选 F,所以上述方案最终还要乘以 2,即:

2 ∗ b [ n − i + 1 ] ∗ 2 ∗ a [ i − 1 ] = 4 ∗ b [ n − i + 1 ] ∗ a [ i − 1 ] 2*b[n-i+1]*2*a[i-1] = 4*b[n-i+1]*a[i-1] 2b[ni+1]2a[i1]=4b[ni+1]a[i1]

综上所述,可得到从中间出发的总方案数为:

4 ∗ ( b [ i ] ∗ a [ n − i ] + b [ n − i + 1 ] ∗ a [ i − 1 ] ) 4 * ( b[i]* a[n-i] + b[n-i+1]*a[i-1] ) 4(b[i]a[ni]+b[ni+1]a[i1])



汇总


综合 1、2 可以得到最终的方案总数为

s u m = 4 ∗ a [ n ] + 4 ∗ ( b [ i ] ∗ a [ n − i ] + b [ n − i + 1 ] ∗ a [ i − 1 ] ) sum = 4*a[n] + 4 * ( b[i]* a[n-i] + b[n-i+1]*a[i-1] ) sum=4a[n]+4(b[i]a[ni]+b[ni+1]a[i1])

下面我们来确定初值。在上面的所有相关公式中,存在的有 a [ i − 1 ] , a [ i − 2 ] , b [ i − 1 ] a[i-1],a[i-2],b[i-1] a[i1]a[i2]b[i1],那么我们就需要确定出 a [ 1 ] 、 a [ 2 ] 、 b [ 1 ] a[1]、a[2]、b[1] a[1]a[2]b[1] 的初值。

  • n = 1 n=1 n=1 时,从某个顶点出发显然只有一种行走方式,因此 a [ 1 ] = 1 a[1]=1 a[1]=1

  • n = 2 n=2 n=2 时,从某个顶点出发会有以下 6 种方式,见下图,因此 a [ 2 ] = 6 a[2]=6 a[2]=6
    在这里插入图片描述

  • n = 1 n=1 n=1时,对于 “一趟过去,一趟返回” 这种走法而言,其从某个顶点出发显然只有一种走法,即走向该列的另一个点;

  • n = 2 n=2 n=2 时,对于 “一趟过去,一趟返回” 这种走法而言,其从某个顶点出发显然只有 2 种方式(见上图中的情况②),因此 b [ 2 ] = 2 b[2]=2 b[2]=2(为了在写代码的时候能够将 a [   ] a[\ ] a[ ] 数组和 b [   ] b[\ ] b[ ] 数组放进同意循环,这里也顺带给出了 b [ 2 ] b[2] b[2] 的值)。



—— 拨云见雾终有时 ——


下面直接给出本题的完整代码:

#include<iostream>
using namespace std;

const int MOD=1000000007;

int main()
{
	int n; cin>>n;
	long long a[1005],b[1005];
	if(n==1){
		cout<<2<<endl;
		return 0;
	}
	// 赋初值
	a[1]=1,a[2]=6;
	b[1]=1,b[2]=2;
	// 打表,递推求出a数组和b数组在不同长度情况下的值=
	for(int i=3;i<=n;i++) 
	{
		b[i]=(2*b[i-1])%MOD; 
		a[i]=(2*a[i-1]+b[i]+4*a[i-2])%MOD;
	}
	// 4个顶点
	// 特别注意这里sum的数据类型需要用long long,否则有可能在乘以4后发生数据溢出
	long long sum=4*a[n] % MOD;	
	// 根据前面算出的a[ ]数组和b[ ]数组,递推出从中间出发时的方案数
	// 注意i的范围是大于1小于n
	for(int i=2;i<n;i++) 
		sum = (sum+4*(b[i]*a[n-i]+b[n-i+1]*a[i-1]))%MOD;
	cout<<sum<<endl; 
	return 0; 
 }


END


  • 55
    点赞
  • 88
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 37
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

theSerein

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值