P1057 [NOIP2008 普及组] 传球游戏

题目描述
上体育课的时候,小蛮的老师经常带着同学们一起做游戏。这次,老师带着同学们一起做传球游戏。

游戏规则是这样的:

n个同学站成一个圆圈,其中的一个同学手里拿着一个球,当老师吹哨子时开始传球,每个同学可以把球传给自己左右的两个同学中的一个(左右任意),当老师再次吹哨子时,传球停止,此时,拿着球没有传出去的那个同学就是败者,要给大家表演一个节目。

聪明的小蛮提出一个有趣的问题:有多少种不同的传球方法可以使得从小蛮手里开始传的球,传了

m次以后,又回到小蛮手里。两种传球方法被视作不同的方法,当且仅当这两种方法中,接到球的同学按接球顺序组成的序列是不同的。比如有三个同学
1
1号、
2
2号、
3
3号,并假设小蛮为
1
1号,球传了
3
3次回到小蛮手里的方式有
1

2

3

1
1→2→3→1 和
1

3

2

1
1→3→2→1,共
2
2 种。

输入格式
一行,有两个用空格隔开的整数

,

(
3



30
,
1



30
)
n,m(3≤n≤30,1≤m≤30)。

输出格式
1
1个整数,表示符合题意的方法数。

输入输出样例
输入 #1复制
3 3
输出 #1复制
2
说明/提示
40%的数据满足:
3



30
,
1



20
3≤n≤30,1≤m≤20

100%的数据满足:
3



30
,
1



30
3≤n≤30,1≤m≤30

2008普及组第三题
UPD:2019/7/7
主要更新了关于DP的讲解,使本文更适合初学者。

UPD:2020/2/12
由于似乎有人看不懂表是怎么打出来的,我就在咕了几个月之后来说明一下。

整理了文章内容,删去了一些无关言论。

改变码风,调整注释,并添加解法说明。

优化阅读体验,将过长且不很必要的代码转移到剪贴板中。节约读者滑滚轮时间。

正文
这道题明眼人都看得出有很多做法,比如搜索、DP等等,似乎还有人用了矩阵乘法。

总而言之,彰显出大佬的强大,但麻烦且对初学者非常不友好。

像我这种蒟蒻,只会打暴力的BFS。。

解释一下思路:先开一个结构体:

struct node
{
int now,state; //当前球的位置和传了几次
node(int a,int b) //这个是一个函数,可以在结构体队列中方便地进行元素入队,待会会讲到
{
now=a,state=b;
}
};
最早我的想法就是,开这样一个队列,将当前球的状态(球传到到第几个人)的往左传和往右传的可能压进队列(特判一下1和n的情况),(当然作为显示传了几次的state要加一)然后扔掉队首元素。

完整代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,i;
long long ans;
struct node
{
int now,state; //now是当前球在谁手中,state是球被传了几次
node(int a,int b)
{
now=a,state=b;
}
};
queue que;
void bfs(int x,int step)
{
if(stepm) //步数已经达到了上限
{
que.push(node(x,step));
return;
} //特判1
if(x
1)
{
que.push(node(n,step+1));
que.push(node(2,step+1));
}
else if(xn) //特判n
{
que.push(node(n-1,step+1));
que.push(node(1,step+1));
}
else
{
que.push(node(x-1,step+1));
que.push(node(x+1,step+1));
}
return;
}
void all() //统计函数,就是最后到了步数都达到上限的时候,统计所有球在1位置的情况
{
while(!que.empty())
{
node xy=que.front();
if(xy.now
1)
ans++;
que.pop();
}
}
int main()
{
// freopen(“ball.in”,“r”,stdin);
// freopen(“ball.out”,“w”,stdout);
cin>>n>>m;
que.push(node(1,0));
while(que.front().state!=m)
{
bfs(que.front().now,que.front().state);
que.pop();
}
all();
cout<<ans<<endl;
return 0;
}
当然,各位dalao都知道,这种做法是不可能AC的。

为什么?(因为这是暴力枚举啊,连剪枝都没有)

来让我们加一个小小的剪枝。

我们可以判断一下,就是当前位置如果连一直往一个方向走都到不了位置1的话,那这个状态也就没用了,可以直接return掉。

贴上剪枝版的BFS中要添加的内容:

if(x-(m-step)>1&&x-(m-step)<n)	//判断是不是到不了位置1
	return;

然后,我们输入数据“5 27”的答案是正确的,运行时间从未剪枝的29.65秒缩短到了26秒多

所以说,普通的搜索+优化是不可行的。

怎么办?
打表!
我第一个想到的方法。

暴力标程都出来了,不打表干嘛呢?有时间再去死磕DP正解啊

用于打表的程序:前往剪贴板查看

于是,就有了接下来的表:前往剪贴板查看

好了,我们的表已经出来了。提交,AC!

(话说洛谷的评测现在似乎每个测评点时间下限从以前的0ms提高到了2ms/3ms?不然打表程序一般情况下怎么可能需要几十ms的时间呢?)

高级优化
本来解法2和解法3都是没有的,直接就到DP了。

但是后来我回顾了一下以前做过的一道题:P1877 音量调节

这是我对该题的题解(已过审):P1877

(宣传博客?算了不管了)

在P1877中,本来我的BFS也是过不了的,但是我后来想到一个优化方法,把BFS给过了,在洛谷的环境下用时28ms,也就是平均每个测试点2~3ms,这个已经和我交上去通过的DP程序差不多了(第一次24ms,后面又交了一次,29ms?)。

这个优化方法就是压缩/合并相同状态

状压DP

其实原理很简单:不再是每个状态用一次BFS,而是每一轮传球用一次BFS,在这一次BFS中将所有这轮传的球统一处理,先全部统计起来,最后统一入队。

这样子的话,我们的队列当中最多最多也只会有30个元素,因为每个位置最多会在当前队列中出现一次。这样空间和时间复杂度是不是就降下来了?

前往剪贴板查看

但是其实如果这么做的话,跟DP就没有本质上的区别了,所以似乎所有的方法最终都指向了DP。

记忆化搜索
记忆化搜索,我们教练是有专门针对这道题讲过的。懒得打,由于年代久远又抄不到教练的标程,而且我翻了翻题解,已经有别人的记搜过审了。oh yeah我可以不放标程了,诸位自行寻找吧

DP
对于DP新手来说,打DP是一件很痛苦的事情。

有一个技巧(?):要从找规律开始。

我们可以发现,任何一个位置都只能从左边和右边传过来,那么他只能从他左边和他右边的同学手上接到球,那球传到他手上的路径数是不是球传到他左边同学的路径数与球传到他右边同学的路径数之和?

有点绕,但是如果你是想认真学习的人,那么希望你能按下性子,认真理解一下这句话。

这样我们就可以列出我们的方程:

f[i][j]=f[i-1][j-1]+f[i-1][j+1]
为什么是这样?

让我们用手模拟一下(假设有5个人,传6次球,为了方便理解,我将其做成了一个环):

在初始情况下,小蛮手中必然有且只有一个球,记为1;

第一轮传球后,小蛮必然将手中的球传给2号或5号同学,于是这两个同学各有1种方式接到球;

第二轮传球后,(如果上一轮小蛮将球传给2号)2号同学必然将球传给小蛮或3号,(如果上一轮小蛮将球传给5号),5号同学必然将球传给小蛮或4号,于是小蛮有2种情况接到球(分别从2号和5号手中);

第三轮及其后以此类推。

我们据图可以发现,假设初始情况为第0行,小蛮为第1列,则有(从第1行开始):

f[i][j]=f[i-1][j-1]+f[i-1][j+1];
只需要特判一下1和n就行了。

贴DP标程:

#include<bits/stdc++.h>
using namespace std;
int f[31][31],i,j,m,n;
int main()
{
cin>>n>>m;
f[0][1]=1;
for(int i=1; i<=m; i++)
for(int j=1; j<=n; j++)
if(j1)
f[i][j]=f[i-1][n]+f[i-1][2];
else if(j
n)
f[i][j]=f[i-1][1]+f[i-1][n-1];
else
f[i][j]=f[i-1][j-1]+f[i-1][j+1];
cout<<f[m][1]<<endl;
return 0;
}
最后,告诫大家:

DP是毒瘤,谁打谁知道。

要想不被毒,暴力少不了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值