不平等博弈详解(含例题HDU3544,POJ2931)

IOI国家队论文:http://ishare.iask.sina.com.cn/f/67940410.html

然后我捞点干货出来写在这

首先是超实数的一些定义定理(我用我自己的话来说了)

①定义 x={XL | XR},大写X表示数集

要求 XL < x < XR (我下面都用这种写法,表示XL中任取,XR中任取) 或者 XL或XR 为空集也行

对 x={XL | XR},y={YL | YR}

定义偏序关系 "<="  x<=y 等价于 XLx,或者 XL 或 YR 为空集也行

这个可以用 XL秒速手推

达利函数(对单元素集合或空集)

不平等博弈详解(含例题HDU3544,POJ2931)_第1张图片

然后这个国家队论文的函数其实写的有点问题,我给稍微修改了下

达利函数:(x为实数,δ(x)为对应的超实数)

\LARGE \delta (x)=\left\{\begin{matrix} \{\; | \;\} , x=0 \\ \{x-1\; | \;\; \},x>0\wedge x \in Z \\ \{\;\;|\;x+1\},x<0\wedge x \in Z \\ \{\frac{j-1}{2^k} \;| \;\frac{j+1}{2^k}\},x=\frac{j}{2^k}\wedge j,k \in Z\wedge k>0 \end{matrix}\right.

然后很明显不止函数书写上的问题,有的超实数在这个函数里无法取到但是也是可算的,这个国家队论文似乎忘说了

比如 { 1/3 | 11/4 } = 1 ,{ -11 | -1/2 } = -1,{-100 | 100} = 0 中间有整数取的是符合条件的离0最近整数

再比如 { 1/8 | 3/4 } = 1/2 ,中间无整数也不符合达利函数要求,取法是取满足条件的 j/2^k,先使 k 尽可能小,再使 j 尽可能小

再比如 { -2 |  } = {  |  99 } = 0 

再比如 {  | 3/2 } = 1,{ -7/4 |  } = -1 

实际上,空集就是 l 视作 无穷小 ,r 视作无穷大,然后变成了有 l 又有 r 的情况处理

这之后我会给出一个最准确的计算方式

然后对于多元素集

x = { XL | XR } = { lmax | rmin } 然后带入达利函数就可以求了

其实直接用结论是最好的,所以我给出一个达利函数的反函数

称为SN函数,记作SN(x),x是超实数,SN是对应实数,同类超实数取最简形式,即 { lmax | rmin }

SN(x)=\left\{\begin{matrix} 0,x=\{\; | \;\}\ \\ max(0,l+1),x=\{l\;|\;\;\} , l\in Z \\ min(0,r-1),x= \{\;\;|\;r\} ,r\in Z \\ \frac{l+r}{2} ,x=\{l\;|\;r\} , r-l<=1 ,log(r-l)\in Z\end{matrix}\right.

这个函数其实也稍微漏掉了些情况,但是用在一些简单情况的手算上还是挺方便的,最准确全面的做法还是应当按下面这样的来

注意,下面这里写的是最完整全面准确的计算方式,可以算出一切超实数所对应的实数

将左空集视作 -inf ,右空集视作 inf,然后用以下方法计算

① l < 0 且 r > 0,SN(x) = 0

② l , r 中间有整数

a. 0 <= l < r,SN(x) = floor ( l+1 )

b. l < r <= 0,SN(x) = ceil ( r-1 )

③ l , r 中间无整数,优先 k 最小,其次 j 最小,使得 l < j / 2^k < r

那么可能有人会问 如果遇到 l > r 的情况怎么办?他甚至不符合定义。

不用担心。博主认为永远不会算出这种情况来的

然后说下什么呢

实际操作的时候往往是三点对应,一个局势(状态) 对应 一个实数 对应 一个超实数

在代码里,我们一般会把自变量写成状态,然后用其对应的超实数 计算 其对应的实数作为因变量

先这样理解一下,看了之后的例题就会有体会了

④加法:

x + y = {XL + y , x + YL | XR + y , x + YR }

⑤相反数:

-x =  { -XR | -XL } 

-0 = 0

a-b = a + (-b)  

⑥超实数加法满足交换律和结合律

⑦与不平等博弈的关系:

a. 玩家L 操作的后继状态的SN值集合 XL,玩家R 操作的后继状态的SN值集合 XR

这个游戏可用超实数 x = { XL | XR } 表示

b.SN定理(这个我自己起的名,请忽视...):多游戏的 SN 等于子游戏的 SN 的直接加和

c. x>0,L必胜 ; x<0,R必胜 ; x=0,后手必胜(一定注意是后手必胜,千万别记反了)

对了,这里多说一嘴,想要提高理解的话,也可以看一下这篇博文https://blog.csdn.net/wozaipermanent/article/details/81559438,非常厉害

然后我们一起来看下下面几道例题

例一:HDU - 3544

N个巧克力 长xi , 高yi,ALICE可竖切,BOB可横切,必须切整数,问输赢

那么首先呢

上面博文里有个你可以参考的东西,我给你放这来

这种杂糅的东西都可以推到根上,不是空状态就是单维状态

比如HDU3544这个题,显然没法推到单维状态,但是空状态还是有的,SN (1,1)={ | }=0

然后用这个就全能推了

比如 SN (2 , 1) = {SN(1,1) + SN(1,1) | } = {0 | } = 1,注意相加的那块,那里是SN定理,要注意

再比如 SN (3 , 1) = {SN(1,1) + SN(2,1) | } = {1 | } =2

再比如 SN (4 , 1) = {SN(1,1) + SN(3,1)  ,  SN(2,1) + SN(2,1) |  } = { 2,2 |  } ={ 2 | } = 3

用类似的方法你就能推出很多东西

不过说实话手算真的累,肯定要写代码算的

然后你发现这个 x,y 的范围都是 1e9 肯定超时

明显是打表找规律,打表还是写个代码打表吧,手算真的累

下面给一个打表代码(基本上可以当做模板

#pragma GCC optimize("Ofast")
#include 
#define inf 0x3f3f3f3f
using namespace std;
double sn[20][20];
double get_sn(int x,int y){
	if(sn[x][y]!=inf) return sn[x][y];
	double l=-inf,r=inf;
	for(int i=1;i<=x/2;i++) l=max(l,get_sn(i,y)+get_sn(x-i,y));
	for(int i=1;i<=y/2;i++) r=min(r,get_sn(x,i)+get_sn(x,y-i));
	if(l<0&&r>0) sn[x][y]=0;
	else if(floor(l+1)>l&&floor(l+1)=0) sn[x][y]=floor(l+1);
		else sn[x][y]=ceil(r-1);
	}
	else{
		int j,k,tmp=1;
		bool ok=0;
		for(k=1;!ok;k++){
			tmp<<=1;
			for(j=floor(l*tmp+1);!ok&&jl*tmp&&j

打出的表:(因为我已经知道结论是全是整数,所以输出.0f 好看点,你也可以输出 .1f .2f,输出一下你就知道后面都是0了)

不平等博弈详解(含例题HDU3544,POJ2931)_第2张图片

然后下面我把别人题解的表格拿过来了,他们画的这个看着更舒服点.......

不平等博弈详解(含例题HDU3544,POJ2931)_第3张图片

然后这个题怎么找规律呢

首先呢你会看到最左列和最上行有着明显规律

中间那些呢,好像都是一块一块的,似乎跟 2^k 有关

然后你就开始去猜想到底怎样的规律

好,停。

规律要怎么找,不是盲目的猜,你要有逻辑有道理地去分析

来,跟我一起看

首先它是关于主对角线对称的负对称矩阵,能发现吧。

于是我们研究下三角阵或者上三角阵即可

来看下三角阵

竖着向下看随 x 的增大 SN 在增大

水平来看,SN 随 y 的增大 而 衰减,基本上呈 2^k 型的衰减和分布

我们来写几个看一下

比如看 x = 14,15 这两行,y = 1,SN = x - 1 = x/1 - 1

y= 2,3,SN = x/2 - 1 = 6

y = 4,5,6,7,SN = x/4 - 1 = 2 这个也可以再看下 x = 12,13 来验证这个规律

那么很明显了  SN(x,y)=\frac{x}{2^{\left \lfloor logy \right \rfloor}}-1

然后上面这个是对下三角而言的,上三角你对称下就好

把大的交换给 x,小的交换给 y,系数乘个 -1 就行

然后算的时候显然你直接 pow(2,y-1) 溢出的无影无踪

处理方法很简单,你让 2 一直倍乘 2,乘到 y-1 次前就比 n 大了结果就很明显 0-1=-1 了,否则就正常就完事了

很简单,看我标程吧

对,还有一点没说,你算的这些SN用SN定理再做个加和就是我们要的答案了

下面给出AC代码(注意C++里面 log 是 ln ,log2 才是 log,同样 log10 是 lg)

#pragma GCC optimize("Ofast")
#include 
using namespace std;
using ll=long long;
int t,n;
ll x,y,ans;
int main(){
	scanf("%d",&t);
	for(int cas=1;cas<=t;cas++){
		ans=0;
		scanf("%d",&n);
		while(n--){
			scanf("%lld%lld",&x,&y);
			int k=1;
			if(x0) printf("Case %d: Alice\n",cas);
		else printf("Case %d: Bob\n",cas);
	}
	return 0;
}

例二:POJ - 2931

这个题是国家队论文的第一个例题。

然后和这篇https://blog.csdn.net/wozaipermanent/article/details/81559438巨佬博文的黑白棋是一样的

t组数据,C1和C2两套塔,每套塔有三个塔,分别n1,n2,n3块砖,从上到下描述,有黑砖和白砖,白玩家可以拿走一个白砖及其上面的所有砖,一个黑玩家可以拿走一个黑砖及其上面的所有砖,白玩家为L玩家,黑玩家为B玩家,问 SN(C1) 是否 >=SN(C2)

这个题呢,你首先想到定义二维状态,一维是白子数,另一维是黑子数,但是好像又有顺序问题

所以呢,你不会打算立刻打表,你打算先手推下,看看规律是怎样的

一开始下面 1 个白子时 SN = { 0 | } = 1

一开始下面 1 个黑子时 SN = {  | 0 } = -1

一开始下面 y 个黑子时 SN = {  | -1,-2,...,-(y-1)  } = {  | -(y-1) } = -y 

一开始下面 x 个白子时,SN= { 1,2,...,x-1 |  } = { x-1 |  } = x

然后你放 1 个黑子 , SN = { 1,2,...,x-1 | x } = { x-1 | x } = x - 1/2

然后比如你放上 2 个黑子 SN = { 1,2,...x-1 | x , (x,1) } = { x-1 | (x,1) } = { x-1 | x - 1/2 } = x - 3/4 = x - 1/2 - 1/4

那么你再放 1 个白子呢 SN = { 1,2,...,x-1,(x,2) | x , (x,1) } = { x - 3/4 | x - 1/2 } = x - 5/8 = x - 1/2 - 1/4 + 1/8

那么根据前车之鉴,再放上几个白子应该都是 + 1/2^k

那么如果我这时又放上黑子呢?

再放 1 个黑子 SN = { 1,...,x-1,(x,2) | x,(x,1),(xW,2B,1W) } = { x - 3/4 | x - 5/8 } = x - 11/16 = x - 1/2 - 1/4 + 1/8 - 1/16

实际上不管怎么交替都无所谓,规律非常明确了

前x个直接 +x/-x,后面的从k=1开始 +1/2^k或-1/2^k,那么W是+,B是-,就这么简单

然后如果你担心double有精度误差,那就通一下分,比如这道题可以集体扩大 2^52 倍

但是有的题无法扩大这么多可能就不得不用分数计算了

实际上对于SN,double应该不会有精度误差,因为分母全是 2^k,学过机组的同学都知道,这种情况的二进制并没有被近似处理,但是也不是说完全不会被近似,如果位数过长呢,超过64位二进制,规格化后就需要对尾数0舍1入

所以说像这种可以通分的题,不通分,就用double,也是可以的

而那种小数位数很多,二进制超过64位的题,就没办法了,必须定义分数去做计算了

下面给出两个AC代码

//#pragma GCC optimize("Ofast")
//#include 
#include 
#include 
using namespace std;
typedef long long ll;
int t,cas,a[55],n[3];
ll ans1,ans2;
char s[10],c;
ll get_sn(int n){
	int i;
	ll res=0,tmp=1ll<<52;
	a[0]=a[1];
	for(i=1;i<=n&&a[i]==a[i-1];i++){
		if(!a[i]) res+=tmp;
		else res-=tmp;
	}
	tmp>>=1;
	for(;i<=n;i++){
		if(!a[i]) res+=tmp;
		else res-=tmp;
		tmp>>=1;
	}
	return res;
}
int main(){
	scanf("%d",&t);
	while(t--){
		ans1=ans2=0;
		scanf("%s %d\n",s,&cas);
		scanf("%d %d %d",&n[0],&n[1],&n[2]);
		for(int i=0;i<3;i++){
			for(int j=1;j<=n[i];j++){
				scanf("%s",s);
                a[j]=s[0]=='W'?0:1;
			}
			ans1+=get_sn(n[i]);
		}
		scanf("%d %d %d",&n[0],&n[1],&n[2]);
		for(int i=0;i<3;i++){
			for(int j=1;j<=n[i];j++){
				scanf("%s",s);
                a[j]=s[0]=='W'?0:1;
			}
			ans2+=get_sn(n[i]);
		}
		if(ans1>=ans2) printf("Test %d: Yes\n",cas);
		else printf("Test %d: No\n",cas);
	}
	return 0;
}
//#pragma GCC optimize("Ofast")
//#include 
#include 
#include 
using namespace std;
typedef long long ll;
int t,cas,a[55],n[3];
double ans1,ans2;
char s[10],c;
double get_sn(int n){
	int i;
	double res=0;
	ll tmp=2;
	a[0]=a[1];
	for(i=1;i<=n&&a[i]==a[i-1];i++){
		if(!a[i]) res++;
		else res--;
	}
	for(;i<=n;i++){
		if(!a[i]) res+=1.0/tmp;
		else res-=1.0/tmp;
		tmp<<=1;
	}
	return res;
}
int main(){
	scanf("%d",&t);
	while(t--){
		ans1=ans2=0;
		scanf("%s %d\n",s,&cas);
		scanf("%d %d %d",&n[0],&n[1],&n[2]);
		for(int i=0;i<3;i++){
			for(int j=1;j<=n[i];j++){
				scanf("%s",s);
                a[j]=s[0]=='W'?0:1;
			}
			ans1+=get_sn(n[i]);
		}
		scanf("%d %d %d",&n[0],&n[1],&n[2]);
		for(int i=0;i<3;i++){
			for(int j=1;j<=n[i];j++){
				scanf("%s",s);
                a[j]=s[0]=='W'?0:1;
			}
			ans2+=get_sn(n[i]);
		}
		if(ans1>=ans2) printf("Test %d: Yes\n",cas);
		else printf("Test %d: No\n",cas);
	}
	return 0;
}

好,那么来思考下,如果这道题你要打表找规律,那么应该怎么打表

博主给出两种方法:

①0表黑,1表黑,二进制串来表示状态,但要记录长度,以区分黑和空

②0表空,1表黑,2表白,三进制串来表示状态

 

你可能感兴趣的:(不平等博弈详解(含例题HDU3544,POJ2931))