洛谷P1857 题解

原题

P1857 质数取石子


思路概述

题意分析

给定 \(n\) 个石子,先后手轮流取 \(x(x∈prime)\) 个石子,最先取不了的一方失败。求先手方是否有必胜策略,如果有,输出步数(遵循必胜方尽快取胜,必败方尽可能拖延步数的策略)。

思路分析

由题意可知,本题是一道博弈论+动态规划算法的题。只需要打表推出 \(n\) 的胜负态 \(f(n)\) 就能知道先手方是否有必胜策略,再通过一定规律分析出必胜、必败方的最优策略即可求解。

由于本题每次取的石子数为质数,所以需先用线性筛法找出给定范围内的所有质数。关于质数线性筛,笔者不多作冗述,有需要可前往:

P3383 【模板】线性筛素数 题解


算法推导

关于胜负态

虽然本题与博弈论有一定关系,但没有系统学习过博弈论(没错就是我)也没有太大影响,只需要对博弈游戏中的必胜/必败态有一定认识即可。

对于一个博弈游戏的任意状态(本题中表示为一元函数 \(f(n)\) )必然属于两类:若当前的先手方无论采用何种策略,都必然失败的状态称为必败态;反之,若先手方采用适当策略,必定能使对方处于必败态的状态称为必胜态。

由胜负态的基本定义可以得出胜负态的转移方程。我们定义博弈游戏的当前状态为 \(f(n)\) (状态可以是一元函数,也可以是多元函数),当前状态的所有前驱状态(前驱状态不一定唯一)为 \(pre_x=\{a_1,a_2...a_n \}\) (特别地,题目预设的游戏结束条件没有前驱状态),同时定义胜态函数值为 \(1\) ,必败态函数值为 \(0\) 。那么易得转移方程(假设边界条件是必败态):

\[f(x)=\begin{cases}1,∃i∈pre_x\text{ and }f(i)=0\\0,\text{otherwise}\end{cases} \]

关于动态规划

  • 有了胜负态转移方程,就可以推导出本题的转移方程。在递推前,先用线性筛选出既定范围内的所有质数\(prime\)。转移方程如下:

\[f(x)=\begin{cases}1,∃i=x-prime_j(j∈N^*,i∈N)\text{ and }f(i)=0\\0,\text{otherwise} \end{cases} \]

关于路径长度计数

由于题目不仅要求输出当前胜负态,并且当此时为必胜态时,还要输出最少步数(满足必胜方尽快取胜必败方尽量拖延的原则)。这就要求在DP过程中还要附带处理关于步数的问题。

由上述转移方程可以知道状态之间的关系可以用一个有向无环图来描述,即两个状态可以由一条有向边链接,而状态的转移就是有向边上的移动。由上述推导就能将尽快取胜/尽量拖延的策略转化为求当前状态对应节点到源节点( \(x=0,1\) )的最短/最长路径走法。并且对于图中的任意节点都存在唯一的最长与最短路径的前驱。因此,我们只需要在每一个新状态进入考虑中的时候更新其最长路径前驱或最短路径前驱,在询问过程中模拟双方按最优策略轮流取石子的过程即可。

由于一个状态 \(f(x)\) 的胜负是一定的,所以对于一个必胜态,无论谁取石子,都遵循尽快取胜的原则,反之亦然。因此,对于一个必胜态,就记录其最短路径前驱;反之,则记录其最长路径前驱。详细过程见代码。


AC code

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<queue>
#include<set>
#include<ctime>
#define RI register int
using namespace std;
const int maxn=2e4+10;
typedef struct
{
	bool ptw;
	int cnx,cni,prx,pri;
	inline void init(int x)
	{
		ptw=0;cnx=-1;cni=0x3f3f3f3f;prx=pri=-1;
		return;
	}
}node;
node e[maxn];
int T,n,rmx,cnt,ans;
int gtn[11],p[maxn],v[maxn];
bool sit;
inline int read();
inline void print(int x);
inline void gtp(int x);/*质数线性筛*/
inline void init(int x);/*预处理状态*/
int main()
{
	T=read();
	for(RI i=T;i>=1;--i) rmx=max(rmx,gtn[i]=read());
	/*由题目性质可知 不需要打出[1,20000]中的所有状态 只需要打出最大数据之前的所有状态即可*/
	gtp(rmx);init(rmx);
	for(;T;--T)
	{
		n=gtn[T];ans=0;
		if(!e[n].ptw) puts("-1");/*必败态*/ 
		else
		{
			sit=1;/*用变量sit来模拟当前是必胜或必败方操作*/
			for(RI i=n;i>1;i=sit?e[i].prx:e[i].pri)
			{
				++ans;
				sit^=1;/*当前方操作完毕 换对方操作*/
			}
			print(ans);putchar('\n');
		}	
	}
	return 0;
}
inline int read(void)
{
	char putin;bool isneg=false;RI ret=0;
	putin=getchar();
	while((putin>'9' || putin<'0') && putin!='-')
		putin=getchar();
	if(putin=='-')
	{
		isneg=true;
		putin=getchar();
	}
	while(putin>='0' && putin<='9')
	{
		ret=(ret<<3)+(ret<<1)+(putin&15);
		putin=getchar();
	}
	return isneg?-ret:ret;
}
inline void print(int x)
{
	if(x<0)
	{
		putchar('-');
		x=-x;
	}
	if(x>9) print(x/10);
	putchar(x%10+'0');
	return;
}
inline void gtp(int x)
{
	memset(v,0,sizeof(v));cnt=0;
	for(RI i=2;i<=x;++i)
	{
		if(!v[i])
		{
			p[++cnt]=i;
			v[i]=i;
		}
		for(RI j=1;j<=cnt && i*p[j]<=x;++j)
			if(p[j]>v[i]) break;
			else v[i*p[j]]=p[j];
	}
	return;
}
inline void init(int x)
{
	for(RI i=1;i<=x;++i) e[i].init(i);
	e[0]=e[1]=(node){0,0,0,0,0}; 
	for(RI i=0;i+2<=x;++i)
		for(RI j=1;j<=cnt && i+p[j]<=x;++j)
		{
			e[i+p[j]].ptw|=(e[i].ptw^1);
			if(e[i].ptw && e[i+p[j]].cnx<e[i].cni+1)/*必胜态 更新最短路径前驱*/
			{
				e[i+p[j]].cnx=e[i].cni+1;
				e[i+p[j]].prx=i;
			}
			else if(!e[i].ptw && e[i+p[j]].cni>e[i].cnx+1)/*必败态 更新最长路径前驱*/
			{
				e[i+p[j]].cni=e[i].cnx+1;
				e[i+p[j]].pri=i;
			}	
		}		
	return;
}

Update

2022.11.18

修复了必胜态与必败态推导部分的表述问题。

posted @ 2022-03-23 21:57  UOB  阅读(109)  评论(0)    收藏  举报