《GMOJ-Senior-3033 石子游戏》题解

题目大意

两人轮流取一堆 n n n个石子,先手不能全部取完,第一次之后每人取的个数不能超过 另一个人上轮取的数 × k \text{另一个人上轮取的数} \times k 另一个人上轮取的数×k,问限售是否有必胜策略。若有,输出输出第一次最小取的石子数。
对于 10 % 10\% 10%的数据 k = 1 k=1 k=1
对于 30 % 30\% 30%的数据 1 ≤ k ≤ 2 1 \leq k \leq 2 1k2
对于 100 % 100\% 100%的数据 2 ≤ n ≤ 1 0 8 , 1 ≤ k ≤ 1 0 5 2 \leq n \leq 10^8,1 \leq k \leq 10^5 2n108,1k105

分析

这是一道博弈论题。
因为此题有部分分,所以我们先从部分分入手。

10分

对于 10 % 10\% 10%的数据有 k = 1 k=1 k=1,所以对方每次拿的数量都不能超过你上次拿的数量。于是我们可以先打一个表找 k = 1 k=1 k=1的规律:

#include<cstdio>
int dfs(int n,int mn)//搜索,返回值:-1表示必输,其他表示必胜策略第一次最小取的石子数
{
	if(n<=0)//结束判断
	{
		return -1;
	}
	for(int i=1;i<=mn;i++)//枚举可行方案
	{
		if(dfs(n-i,i)==-1)//判断是否必胜
		{
			return i;//必胜
		}
	}
	return -1;//无法胜利,即必败
}
int main()
{
	for(int i=1;i<=20;i++)
	{
		int t=dfs(i,i-1);
		if(t!=-1)
		{
			printf("%d\n",t);
		}
		else
		{
			printf("lose\n");
		}
	}
	return 0;
}

运行代码得:

n n n结果 n n n结果
1 1 1 l o s e lose lose 11 11 11 1 1 1
2 2 2 l o s e lose lose 12 12 12 4 4 4
3 3 3 1 1 1 13 13 13 1 1 1
4 4 4 l o s e lose lose 14 14 14 2 2 2
5 5 5 1 1 1 15 15 15 1 1 1
6 6 6 2 2 2 16 16 16 l o s e lose lose
7 7 7 1 1 1 17 17 17 1 1 1
8 8 8 l o s e lose lose 18 18 18 2 2 2
9 9 9 1 1 1 19 19 19 1 1 1
10 10 10 2 2 2 20 20 20 4 4 4

观察结果,我们可以发现:必输的局面的石子数都可表示为 2 i 2^i 2i i i i为非负整数),即必输的局面为 n = 1 , 2 , 4 , 8 , 16 ⋯ n=1,2,4,8,16 \cdots n=1,2,4,8,16时。同时,必胜策略第一次最小取的石子数就是 n n n二进制下从右往左数第一个“ 1 1 1”所代表的十进制数字,如:
3 ( 10 ) = 1 1 ( 2 ) ⇒ n = 3 时的答案为 1 ( 2 ) = 1 ( 10 ) 3_{(10)}=11_{(2)} \Rightarrow n=3 \text{时的答案为} 1_{(2)}=1_{(10)} 3(10)=11(2)n=3时的答案为1(2)=1(10)
6 ( 10 ) = 11 0 ( 2 ) ⇒ n = 6 时的答案为 1 0 ( 2 ) = 2 ( 10 ) 6_{(10)}=110_{(2)} \Rightarrow n=6 \text{时的答案为} 10_{(2)}=2_{(10)} 6(10)=110(2)n=6时的答案为10(2)=2(10)
1 0 ( 10 ) = 101 0 ( 2 ) ⇒ n = 10 时的答案为 1 0 ( 2 ) = 2 ( 10 ) 10_{(10)}=1010_{(2)} \Rightarrow n=10 \text{时的答案为} 10_{(2)}=2_{(10)} 10(10)=1010(2)n=10时的答案为10(2)=2(10)……
其实这可以被证明:

n n n转成二进制,例如 n = 2 5 ( 10 ) = 1100 0 ( 2 ) n=25_{(10)}=11000_{(2)} n=25(10)=11000(2)

那么我们每次拿掉最末尾的一个 1 1 1,例子拿完后 n = 2 4 ( 10 ) = 1100 0 ( 2 ) n=24_{(10)}=11000_{(2)} n=24(10)=11000(2)

因为对方每次拿的数量都不能超过你上次拿的数量,所以对方不能拿更高层的 1 1 1。这样我们每一次都拿走最末尾的 1 1 1,对方就只能拿 0 0 0。例子中对方拿完后 n = 2 3 ( 10 ) = 1011 1 ( 2 ) n=23_{(10)}=10111_{(2)} n=23(10)=10111(2)

而对方每次拿完 0 0 0后又会产生新的 1 1 1,一直到最后一个 1 1 1被我们拿走。

必败情况就是 2 i 2^i 2i i i i为非负整数),例如: n = 3 2 ( 10 ) = 10000 0 ( 2 ) n=32_{(10)}=100000_{(2)} n=32(10)=100000(2)

因为不能一次性拿完,那么我们只能拿走二进制中的 0 0 0,对方就可以每次拿走二进制末尾的 1 1 1

——改编自 C S D N CSDN CSDN博客

由我们发现的规律可得 10 10 10分的代码:

#include<cstdio>
int lowbit(int x)//求x二进制下从右往左数第一个“1”所代表的十进制数字
{
	return x&-x;
}
int main()
{
	int T;
	scanf("%d",&T);
	for(int Case=1;Case<=T;Case++)//注意多组数据
	{
		printf("Case %d: ",Case);
		int n,k;
		scanf("%d%d",&n,&k);//读入n,k
		if(k==1)//当k=1时
		{
			if(lowbit(n)==n)//当n二进制下从右往左数第一个“1”所代表的十进制数字为n,即n可表示为2^i(i为非负整数)时
			{
				printf("lose\n");//必输
			}
			else
			{
				printf("%d\n",lowbit(n));//有必胜策略
			}
		}
	}
	return 0;
}
30分

对于 30 % 30\% 30%的数据有 1 ≤ k ≤ 2 1 \leq k \leq 2 1k2。其中 k = 1 k=1 k=1的情况我们已经在 10 % 10\% 10%的数据中讨论了,所以我们考虑 k = 2 k=2 k=2的情况。
同理,我们也可以打一个表找 k = 2 k=2 k=2的规律:

#include<cstdio>
int dfs(int n,int mn)//搜索,返回值:-1表示必输,其他表示必胜策略第一次最小取的石子数
{
	if(n<=0)//结束判断
	{
		return -1;
	}
	for(int i=1;i<=mn;i++)//枚举可行方案
	{
		if(dfs(n-i,i*2)==-1)//判断是否必胜
		{
			return i;//必胜
		}
	}
	return -1;//无法胜利,即必败
}
int main()
{
	for(int i=1;i<=20;i++)
	{
		int t=dfs(i,i-1);
		if(t!=-1)
		{
			printf("%d\n",t);
		}
		else
		{
			printf("lose\n");
		}
	}
	return 0;
}

运行代码得:

n n n结果 n n n结果
1 1 1 l o s e lose lose 11 11 11 3 3 3
2 2 2 l o s e lose lose 12 12 12 1 1 1
3 3 3 l o s e lose lose 13 13 13 l o s e lose lose
4 4 4 1 1 1 14 14 14 1 1 1
5 5 5 l o s e lose lose 15 15 15 2 2 2
6 6 6 1 1 1 16 16 16 3 3 3
7 7 7 2 2 2 17 17 17 1 1 1
8 8 8 l o s e lose lose 18 18 18 5 5 5
9 9 9 1 1 1 19 19 19 1 1 1
10 10 10 2 2 2 20 20 20 2 2 2

观察结果,我们可以发现:必输的局面的石子数都在斐波那契数列( F i b o n a c c i   s e q u e n c e Fibonacci \space sequence Fibonacci sequence)上,即必输的局面为 n = 1 , 2 , 3 , 5 , 18 , 13 ⋯ n=1,2,3,5,18,13 \cdots n=1,2,3,5,18,13时。同时,必胜策略第一次最小取的石子数就是 n n n表示成斐波那契数列中若干个互不相邻的数的和后,选出的数中的最小数,如:
6 = 1 + 5 ⇒ n = 6 时的答案为 1 6=1+5 \Rightarrow n=6 \text{时的答案为} 1 6=1+5n=6时的答案为1
16 = 3 + 13 ⇒ n = 16 时的答案为 3 16=3+13 \Rightarrow n=16 \text{时的答案为} 3 16=3+13n=16时的答案为3
20 = 2 + 5 + 13 ⇒ n = 20 时的答案为 2 20=2+5+13 \Rightarrow n=20 \text{时的答案为} 2 20=2+5+13n=20时的答案为2……
这个的证明如下:

因为斐波那契数列有一个性质:把任意一个数 n n n解成数列中互不相邻的数相加,分解出的数两两都相差 2 2 2倍以上(不包括 2 2 2倍),

所以我们把 n n n分解成斐波那契数列后,先手取出最小数个石子后,对方就无法取出次小数个石子,而先手可以取到第 最小数 + 次小数 \text{最小数} + \text{次小数} 最小数+次小数个石子;

如此循环,则先手可以取完这 n n n个石子。

——改编自 C S D N CSDN CSDN博客

由我们发现的规律可得 30 30 30分的代码:

#include<cstdio>
int lowbit(int x)//求x二进制下从右往左数第一个“1”所代表的十进制数字(k=1时用)
{
	return x&-x;
}
int fib[10000001];//斐波那契数列(k=2时用)
int main()
{
	int T;
	scanf("%d",&T);
	for(int Case=1;Case<=T;Case++)//注意多组数据
	{
		printf("Case %d: ",Case);
		int n,k;
		scanf("%d%d",&n,&k);//读入n,k
		if(k==1)//当k=1时
		{
			if(lowbit(n)==n)//当n二进制下从右往左数第一个“1”所代表的十进制数字为n,即n可表示为2^i(i为非负整数)时
			{
				printf("lose");//必输
			}
			else
			{
				printf("%d",lowbit(n));//有必胜策略
			}
		}
		else 
		{
			if(k==2)//当k=2时
			{
				fib[0]=1;//递推求斐波那契数列
				fib[1]=1;
				int len=1;
				while(fib[len]<n)
				{
					fib[len+1]=fib[len]+fib[len-1];
					len++;
				}
				if(fib[len]==n)//当n在斐波那契序列中时
				{
					printf("lose");//必输
				}
				else
				{
					for(int i=len-1;;i--)//分解n
					{
						if(n>=fib[i])
						{
							n=n-fib[i];
							if(n==0)//当找到最小数时
							{
								printf("%d",fib[i]);//输出
								break;
							}
						}
					}
				}
			}
		}
		printf("\n");
	}
	return 0;
}
100分

既然 30 % 30\% 30%的数据都与数列有关,那么 100 % 100\% 100%的数据是不是也与数列有关呢?我们尝试寻找,却没有发现。于是我们要构造一个。
我们设数列 a a a为我们构造的数列,他满足可以选这个序列中的一些数相加来表示任意正整数,并且选出的每两个数都满足其中一个是另一个的 k k k倍以上(不包括 k k k倍)。同时,我们设数列 b b b,其中 b i b_i bi为用 a 1 , a 2 , ⋯   , a i a_1,a_2, \cdots , a_i a1,a2,,ai中的一些数相加能表示的最大正整数,其中选出的每两个数都满足其中一个是另一个的 k k k倍以上(不包括 k k k倍)。初始时, a 1 = b 1 = 1 a_1=b_1=1 a1=b1=1
然后我们来推 a i , b i a_i,b_i ai,bi i ≥ 2 i \geq 2 i2)的表达式。由序列 b b b的定义可知,用 a 1 , a 2 , ⋯   , a x a_1,a_2, \cdots , a_x a1,a2,,ax中的一些数相加能表示的最大正整数为 b x b_x bx,其中选出的每两个数都满足其中一个是另一个的 k k k倍以上(不包括 k k k倍),则 b x + 1 b_x+1 bx+1就无法用 a 1 , a 2 , ⋯   , a x a_1,a_2, \cdots , a_x a1,a2,,ax按上述规则表示。那么我们要将 b x + 1 b_x+1 bx+1加入序列 a a a中,由此可得 a i a_i ai的表达式: a i = b i − 1 + 1 a_i=b_{i-1}+1 ai=bi1+1。又因为 b i b_i bi要满足最大,所以我们要找一个最大的 t t t,使得 a t × k < a i a_t \times k < a_i at×k<ai,则 b i = b t + a i b_i=b_t+a_i bi=bt+ai。但是有时我们找不到一个满足要求的 t t t,这时的 b i b_i bi就为 a i a_i ai
举个例子:

k = 3 k=3 k=3时,有:

i i i a i a_i ai b i b_i bi i i i a i a_i ai b i b_i bi
1 1 1 1 1 1 1 1 1 6 6 6 8 8 8 10 10 10
2 2 2 2 2 2 2 2 2 7 7 7 11 11 11 14 14 14
3 3 3 3 3 3 3 3 3 8 8 8 15 15 15 20 20 20
4 4 4 4 4 4 5 5 5 9 9 9 21 21 21 28 28 28
5 5 5 6 6 6 7 7 7 10 10 10 29 29 29 39 39 39

这样一来,我们就可以轻松求解了:与 30 % 30\% 30%的数据同理,若 n n n在序列 a a a中,则先手必输;否则用 a a a中的数分解 n n n,且分解出的每两个数都满足其中一个是另一个的 k k k倍以上(不包括 k k k倍),必胜策略的第一次最小取的石子数就是分解出的最小数。
代码如下:

#include<cstdio>
int a[10000001],b[10000001];//序列a,b
int main()
{
	int T;
	scanf("%d",&T);
	for(int Case=1;Case<=T;Case++)//注意多组数据
	{
		printf("Case %d: ",Case);
		int n,k;
		scanf("%d%d",&n,&k);//读入n,k
		a[1]=b[1]=1;//初始化序列a,b
		int len;
		for(len=1;a[len]<n;len++)//构造序列a,b
		{
			a[len+1]=b[len]+1;//构造序列a
			if(a[1]*k>=a[len+1])//当选序列a中的最小数(a[1])后仍无法选最后一个数时
			{
				b[len+1]=a[len+1];//b[i]=a[i]
			}
			else
			{
				int l=1,r=len;//二分查找t,因为a具有单调递增性
				while(l<r)
				{
					int mid=(l+r+1)/2;
					if(a[mid]<double(a[len+1])/k)//原来是a[mid]*k<a[len+1](判断选了a[mid]后能否选a的最后一个),注意防止int溢出
					{
						l=mid;//能选,再往后找
					}
					else
					{
						r=mid-1;//能选,往前找
					}
				}
				b[len+1]=b[l]+a[len+1];//b[i]=b[t]+a[i]
			}
		}
		if(n==a[len])//当n在序列a中时
		{
			printf("lose");//必输
		}
		else
		{
			for(int i=len-1;i>=1;i--)//分解n
			{
				if(n>=a[i])
				{
					n-=a[i];
					if(n==0)
					{
						printf("%d",a[i]);//输出最小数
						break;
					}
				}
			}
		}
		printf("\n");
	}
	return 0;
}

总结

做博弈论的题时,要先从小范围的数据中找规律,然后再推广到大范围的数据得出结论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在信号处理领域,DOA(Direction of Arrival)估计是一项关键技术,主要用于确定多个信号源到达接收阵列的方向。本文将详细探讨三种ESPRIT(Estimation of Signal Parameters via Rotational Invariance Techniques)算法在DOA估计中的实现,以及它们在MATLAB环境中的具体应用。 ESPRIT算法是由Paul Kailath等人于1986年提出的,其核心思想是利用阵列数据的旋转不变性来估计信号源的角度。这种算法相比传统的 MUSIC(Multiple Signal Classification)算法具有较低的计算复杂度,且无需进行特征值分解,因此在实际应用中颇具优势。 1. 普通ESPRIT算法 普通ESPRIT算法分为两个主要步骤:构造等效旋转不变系统和估计角度。通过空间平移(如延时)构建两个子阵列,使得它们之间的关系具有旋转不变性。然后,通过对子阵列数据进行最小二乘拟合,可以得到信号源的角频率估计,进一步转换为DOA估计。 2. 常规ESPRIT算法实现 在描述中提到的`common_esprit_method1.m`和`common_esprit_method2.m`是两种不同的普通ESPRIT算法实现。它们可能在实现细节上略有差异,比如选择子阵列的方式、参数估计的策略等。MATLAB代码通常会包含预处理步骤(如数据归一化)、子阵列构造、旋转不变性矩阵的建立、最小二乘估计等部分。通过运行这两个文件,可以比较它们在估计精度和计算效率上的异同。 3. TLS_ESPRIT算法 TLS(Total Least Squares)ESPRIT是对普通ESPRIT的优化,它考虑了数据噪声的影响,提高了估计的稳健性。在TLS_ESPRIT算法中,不假设数据噪声是高斯白噪声,而是采用总最小二乘准则来拟合数据。这使得算法在噪声环境下表现更优。`TLS_esprit.m`文件应该包含了TLS_ESPRIT算法的完整实现,包括TLS估计的步骤和旋转不变性矩阵的改进处理。 在实际应用中,选择合适的ESPRIT变体取决于系统条件,例如噪声水平、信号质量以及计算资源。通过MATLAB实现,研究者和工程师可以方便地比较不同算法的效果,并根据需要进行调整和优化。同时,这些代码也为教学和学习DOA估计提供了一个直观的平台,有助于深入理解ESPRIT算法的工作原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值