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 等价于 XL
这个可以用 XL
③达利函数(对单元素集合或空集)
然后这个国家队论文的函数其实写的有点问题,我给稍微修改了下
达利函数:(x为实数,δ(x)为对应的超实数)
然后很明显不止函数书写上的问题,有的超实数在这个函数里无法取到但是也是可算的,这个国家队论文似乎忘说了
比如 { 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 }
这个函数其实也稍微漏掉了些情况,但是用在一些简单情况的手算上还是挺方便的,最准确全面的做法还是应当按下面这样的来
注意,下面这里写的是最完整全面准确的计算方式,可以算出一切超实数所对应的实数
将左空集视作 -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了)
然后下面我把别人题解的表格拿过来了,他们画的这个看着更舒服点.......
然后这个题怎么找规律呢
首先呢你会看到最左列和最上行有着明显规律
中间那些呢,好像都是一块一块的,似乎跟 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 来验证这个规律
然后上面这个是对下三角而言的,上三角你对称下就好
把大的交换给 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表白,三进制串来表示状态