组合数学(下):概率、博弈

概率

有限概率

👉饱和式救援

【题目】
空间限制: 65536K
● 题目描述
在《流浪地球》电影中,地球上大部分的行星发动机被摧毁。
人类再一次展开全球性救援,现在告诉你每只救援队的目标发动机的编号以及这只救援队在成功救援的概率,假如有至少k个行星发动机能够得到重启,则认为地球会被拯救。
● 输入描述
第一行给出N,M,K。N代表人类派出的救援队总数,M代表被摧毁的行星发动机,K代表至少需要重启的行星发动机总数。(1<=N<=1e5,K<=M<=2000)
接下来N行,每行给出ai,pi,分别代表第i支救援队的目标发动机的编号是ai,救援成功的概率为pi。(1<=ai<=M,0<=pi<=1)
只要有一只救援队顺利抵达该行星发动机,则认为该发动机被成功重启。
● 输出描述
输出地球被救援成功的概率(请严格保留3位小数)
● 示例1
输入

3 2 2
1 1
1 1
2 0.5

输出

0.500

【思路】
称发动机被成功重启为“发动机成功”,否则为“发动机失败”。
P(地球被救援成功)=P(至少k台发动机成功)=i=kmP(有i台成功)
动态规划

设dp[i][j]为P(在i台中有j台成功),则
dp[i][j]=dp[i-1][j]*P(发动机i失败)+dp[i-1][j-1]* P(发动机i成功),

用自然语言描述就是
在i台中有j台成功的概率=在i-1台中有j台成功且i失败的概率+ 在i-1台中有j-1台成功且i成功的概率
dp[i][j]的值仅依赖于dp[i-1][j]和dp[i-1][j-1],因此以递增i的方式计算。
空间复杂度优化:
压缩至一维,则dp[i][j]=dp[i][j]*P(发动机i失败)+dp[i][j-1]* P(发动机i成功),此时需要以递减j的方式计算。

#include<stdio.h>
int main(){
	int n,m,k;//n代表救援队总数,m代表故障发动机总数,k代表最少需要重启的发动机数量
	double s[2005]={0};//s[i]存储i号发动机成功概率
	double ss[2005]={0};
    scanf("%d%d%d",&n,&m,&k);
    for(i=0;i<=m;i++) f[i] = 1.0;
    int a;double p;
    while(n--)
		{scanf("%d%lf",&a,&p);//a代表当前发动机编号,p代表当前成功概率
		p[a]=p[a]*(1.0-p);}//更新失败概率
    for (i=1;i<=m;i++) s[i]=1.0-f[i];//计算成功概率
    ss[0] = 1;
    for(int i = 1; i <= m ;++i){//i从1到m递增 
        for(int j = i; j >= 1; --j)//j从i到1递减 
            ss[j] = ss[j]*f[i]+ss[j-1]*s[i];
        ss[0] = ss[0]*f[i];
    }
    double es = 0;//es代表至少有K台成功的概率 
    for(int i = k; i <= m; ++i)
        es += ss[i];//ss[i]代表在m台中有i台成功的概率 
    printf("%.3f\n",es);
    return 0;
}

【归纳】dp[i][j]=P(i个独立事件中发生j个)=dp[i-1][j]*P(事件i不发生)+dp[i-1][j-1]* P(事件i发生)

无限概率

👉Eddy Walker

【题目】
把N个标记在地上围成一圈,任意指定一个起点和绕行方向,给这些标记依次从0到N-1编号。机器人从任意位置出发,第一步走到标记0,之后每一步都随机选择向前或向后移动一个标记,当所有标记都被机器人走过后,行走结束。用(N,M)来记录一次行走,其中M代表行走结束的位置。
输入T次记录,输出T行,其中第i行输出前i次行走都发生的概率。输出答案对1000000007取模的值。

【思路】
特殊情况:结束位置为0当且仅当N==1时为必然事件,N>1时为不可能事件。
用计算机模拟试验,用频率逼近概率。
用p(i)代表事件i的概率,事件i:结束位置为i。其等效于通过若干步走到i-1(不经过i),再通过若干步绕一个圈走到i+1,再走到i。一共有n-1种终点,这些终点地位相同,所以p(i)=1/(n-1)(1≤i≤n-1)
各次行走之间彼此互相独立,因此前i次行走都发生的概率=前i-1次行走都发生的概率×第i次行走发生的概率。
优化:只要有一次行走不可能发生,接下来的结果都是0。

#include<stdio.h>
#define LL long long
const int mod=1000000007;
LL fraction_mod(LL a,LL b){
    LL ksm=1,x=b,y=mod-2;
    for (;y;y>>=1,x=x*x%mod)
        if (y&1)(ksm*=x)%=mod;
    return a*ksm%mod;
}
int main(){
	LL ans=1;
	int t;scanf("%d",&t);
	while(t--){
		int n,m;
		scanf("%d%d",&n,&m);
		if(ans!=0){
			if(m==0)
				{if(n>1) ans=0;}
			else ans=ans*fraction_mod(1,n-1)%mod;
		}
		printf("%lld\n",ans);
	}
	return 0;
}

概率×几何

👉Random Point in Triangle

【题目】输入A,B,C三个点的坐标,P是三角形ABC中任意一点,计算m=max{ SPAB,SPBC,SPCA }的期望E,输出36*E的值。(保证它是整数)

【思路】
临界点:重心。设从A,B,C出发的中线的另一端点分别为A,B,C,三角形重心为O,通过推导,可画出max函数各分段对应的区域,是3个四边形,如图1。
连续概率:点P只可能落到这3个四边形中的一个(边界情况可忽略),所以可将总期望分解为3个区域中的期望之和。(1)以四边形ADGF为例,要计算出其内部所有的点P与BC连线构成的三角形面积的期望。约定A到BC距离为6h,BC=2l。以DF为界分成上下两部分,分别进行讨论。
(1)根据几何概型,随机点P落到两个区域△ADF与△GDF的概率之比等于它们的面积之比3:1;计算出的期望需要除以△ABC面积。所以有四边形的全期望公式
E 1 = [ 3 4 E ( S D F 上 ) +   3 4 E ( S D F 下 ) ] / S A B C E_1=[\frac{3}{4}E(S_{DF上})+\ \frac{3}{4}E(S_{DF下})]/S_{ABC} E1=[43E(SDF)+ 43E(SDF)]/SABC
(2) 用 h ′ ‾ 代表取得的 h ’的平均值, 用\overline{h\prime}代表取得的h’的平均值, h代表取得的h的平均值,因为三角形内部任意点到指定边距离的期望等于指定边上的高×1/3,所以对于DF上,h’=3h/3=h,E(SDF上)=½BC·(3h+h’)=4hl;对于DF下,h’=h/3,E(SDF下)=½BC·(3h-h’)=8hl/3。所以 E 1 = 11 3 h l / 6 h l = 11 18 S E_1=\frac{11}{3}hl/6hl=\frac{11}{18}S E1=311hl/6hl=1811S。(S为△ABC面积)(3)因为3个四边形面积相等,具有对称性,所以它们地位相等,△ABC的全期望公式
E = 1 3 × E 1 + 1 3 × E 1 + 1 3 × E 1 = E 1 E=\frac{1}{3}\times E_1+\frac{1}{3}\times E_1+\frac{1}{3}\times E_1=E_1 E=31×E1+31×E1+31×E1=E1
(4)答案为36×E1=22S,S可用三角形面积的坐标计算公式计算。

#include<stdio.h>
#define LL long long
int main(){
    LL x1,y1,x2,y2,x3,y3;
    while(scanf("%lld%lld%lld%lld%lld%lld",&x1,&y1,&x2,&y2,&x3,&y3)!=EOF){
        LL s=(x1-x2)*(y1-y3)-(x1-x3)*(y1-y2);
        if(s<0) s=-s;
        printf("%lld\n",11*s);
    }
    return 0;
}

博弈

Bash 博弈(简单博弈)

👉HDOJ-1846 Brave Game

【题目】2人进行一个取石子游戏,有数量为n的一堆石子,两人轮流从中取走1~m个,最先取光石子的一方获胜。
如果游戏的双方使用的都是最优策略,请输出先走还是后走的人能赢。

【分析】当石子数恰好剩下(m+1)时,上一次取的人必定获胜。进一步倒推,第一个使石子数达到(m+1)的整数倍的人必定获胜。所以关键在于初始值n:n% (m + 1) !=0,则先手胜;否则,后手胜。

#include<iostream>
using namespace std;
int main(){
	int c;cin>>c;
	while(c--){
		int n,m;//n为石子总数,m为一次最多可以取走的石子数
		cin>>n>>m;
		if(n%(m+1)!=0) cout<<"first\n";
		else cout<<"second\n";
	}
	return 0;
}

👉HDOJ-2149 Public Sale

【题目】
甲乙两个人在拍卖会上竞价,规则如下:刚开始底价为0,两个人轮流开始加价,每次加价的幅度要在1~N之间,当价格大于或等于成本价 M 时,这次叫价的人就赢得竞拍。假设甲乙两人每次都以对自己最有利的方式进行加价,由甲先开始加价,试求甲第一次要出多少价格才能赢得竞拍。

【思路】
本题是上一题的逆过程。
当出价与成本价恰好相差(n+1)时,上一次取的人必定获胜。进一步倒推,第一个使出价与成本价恰好相差(n+1)的整数倍的人必定获胜。所以关键在于初始值n:m%(n+1)!=0,则先加价者胜;否则,后加价者胜。

#include<iostream>
using namespace std;
int main(){
	int m,n;//m代表成本价,n代表加价上限 
	while(cin>>m>>n){
		if(m<=n) //此时可能有多解
		{cout<<m++;
		for(;m<=n;m++) cout<<" "<<m;}
		else if(m%(n+1)==0) cout<<"none";
		else cout<<m%(n+1);
		cout<<"\n";
	}
	return 0;
}

Wythoff 博弈

有2堆石子,每次需要从其中1堆取至少1颗,或者从两堆取相同的数量,最后取完的人获胜。
用数对(a,b)来记录这2堆石子的状态(a<b),表示第一堆剩下a,第二堆剩下b。如果某个人遇到了状态(0,0),那么他就输了。像这样的必输状态叫做奇异局势。前几个奇异局势是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。用数组a[k],b[k]保存奇异局势,其中k是奇异局势的序号,第一个奇异局势k=0,a[0]=b[0]=0。可以证明, a [ k ] = I N T ( 1 + 5 2 k ) , b [ k ] = a [ k ] + k , a[k]=INT(\frac{1+\sqrt5}{2}k),b[k]= a[k] + k, a[k]=INT(21+5 k)b[k]=a[k]+k其中INT(X)表示取小数X的整数部分。

奇异局势的性质:

  1. 任何自然数都包含在唯一的一个确定的奇异局势中。
  2. 任意操作都可将奇异局势变为非奇异局势。
  3. 采用适当的方法,可以将非奇异局势变为奇异局势。

👉HDOJ-2177 取(2堆)石子游戏

【题目】
有2堆石子(输入保证前者数量<=后者),2人按上述规则进行游戏,假设双方都采取最好的策略,问先手是胜者还是败者。如果胜利,输出后手在先手第一次取子后面临的奇异局势。(代表局势的两个分量前者<后者;如果有2种走法,先输出同取)
Sample Input
1 2
5 8
4 7
2 2
0 0
Sample Output
0
1
4 7
3 5
0
1
0 0
1 2

【分析】
比赛的输赢由初始状态决定:先手面对的是奇异局势,则他的任意操作都会将其变为非奇异局势,后手通过适当操作又变回奇异局势,他必输;先手面对的是非奇异局势,则他可通过适当操作将其变成奇异局势,他必胜。所以问题转化为判断局势的奇异性。
设两堆石子数量为a和b(a<=b),并令k=b-a 。
(1)若a[k]==a,则b[k]=a+k=b,(a,b)为奇异。
(2)若a[k]<a,则b[k]=a+k<b,可能有同取解:可能存在c,使(a-c,b-c)与(a[k],b[k])等价。
(3)若a[k]>a,则b[k]=a+k>b,不可能有同取解
接下来求单取解,方法是将(a,b)与所有奇异局势依次比对,直到两者有一个分量相同。(注意:由于输出时a[k]<b[k],分量不一定对应,见样例第2组)由于输入有多组,可进行预处理,先生成所有的奇异局势。

#include <iostream>
#include <math.h>
#include <cstdio>
#include <cstdlib>
using namespace std;
const int N=1000000;
int a[N+5],b[N+5];
int produce(){
	int k=0;
    for(;;k++){
        a[k]=k*(1.0+sqrt(5.0))/2.0;
		b[k]=a[k]+k;
		if(a[k]==N||b[k]==N) break;
	}
	b[0]=0;//修正未知错误
	return k;
}
int main()
{
	int km=produce();
    int pa,pb,kd,ad;
    while(scanf("%d%d",&pa,&pb),pa+pb){
        kd=pb-pa;
        ad=kd*(1.0+sqrt(5.0))/2.0;//a[kd]
        if(pa==ad) printf("0\n");//奇异局势
        else{//非奇异局势 
            printf("1\n");
            if(ad<pa){//同取
				for(int k=0;k<=km;k++){
					if(a[k]==ad){
						if(pa-a[k]==pb-b[k])
	            			printf("%d %d\n",a[k],b[k]);
	            		break;
	            	}
	            }
	        }
	        for(int k=0;k<=km;k++){//单取 
	            if(pa==a[k]&&pb>b[k]||pa==b[k]||pb==b[k]&&pa>a[k])
	            {printf("%d %d\n",a[k],b[k]);break;}
	        }
	    }
	}
	return 0;
}

【总结】

  1. 进行预处理时,注意以下几点:
    最好用for循环,for循环第一句不要和重复定义循环变量,第二句当上限未知时不要写,第三句步长不要写在循环体里。
    后续检索时,注意循环变量上限。
  2. 用好样例,多组输入分组进行。
  3. 若要改动程序以调试,做好标记。

Nimm 博弈(尼姆博弈)

有m堆石子,双方轮流从中取子, 每一步只能从某一堆中取走部分或全部石子,取到最后一枚石子的人获胜。

当博弈为平衡状态时,先手必败;反之,先手必胜。

平衡状态判定n[1]⊕n[2]⊕…⊕n[m]==0,其中⊕表示异或运算,C++运算符为’^’。

平衡状态性质:奇异态与非奇异态之间相互转换均只需要一步,从非奇异态向奇异态转换的方法是,依次做异或运算,当进行到某一项的结果小于该项的值时,将该项的值变为结果值。

👉HDOJ-2176 取(m堆)石子游戏

【题目】有m堆石子,2人按以上规则进行游戏。先取者负输出No,否则输出Yes他第一步取子的方法(如果从有a个石子的堆中取若干个后剩下b个后会胜就输出a b)。

#include<stdio.h>
#define N 200000
int a[N+50];
int main(){
    int n;
    while(scanf("%d",&n) && n){
        int sum=0;
        for(int i=0;i<n;i++){
            scanf("%d",&a[i]);
            sum^=a[i];
        }
        if(sum==0) printf("No\n");
        else{ 
            printf("Yes\n");
            for(int i=0;i<n;i++){
                int s=sum^a[i];
                if(s<a[i])
                    printf("%d %d\n",a[i],s);
            }
        }
    }
    return 0;
}

👉HDOJ-1849 Rabbit and Grass

【题目】
一个简单下棋游戏规则如下:

  1. 棋盘包含1×n个方格,方格从左到右分别编号为0,1,2,…,n-1;
  2. m个棋子放在棋盘的方格上,方格可以为空,也可以放多个棋子;
  3. 双方轮流走棋,每一步可以选择任意一个棋子向左移动到任意的位置。
  4. 如果所有的棋子都位于最左边(即编号为0的位置),则游戏结束,并且最后走棋的一方为胜者。
    假设双方都采取最优策略,试判断先手的输赢。

【分析】
本题可抽象成与上一题相似的模型,过程如下:
某堆的石子数量=某个位置的棋子的编号
取走某堆中的一颗石子=将某个棋子移动一格
取光某堆=将某个棋子移到最左边
石子全部取光=棋子全部移到最左边

#include<stdio.h>
#define N 1000
int a[N+5];
int main(){
    int n;
    while(scanf("%d",&n) && n){
        int sum=0;
        for(int i=0;i<n;i++){
            scanf("%d",&a[i]);
            sum^=a[i];
        }
        if(sum==0) printf("Grass Win!\n");
        else printf("Rabbit Win!\n");
    }
    return 0;
}

【总结】
常见博弈问题中通常会在0附近存在特解,而其他状态会通过几次博弈达到这些特解。
把特解找出并推算几次后面的博弈即可找出规律。

其他

👉Rikka with Game

【题目】
给出一个由小写字母组成的字符串s,2位玩家R和Y轮流操作字符串(从R开始)。每次进行操作的玩家有2个选择:结束游戏或将s中的一个字符si的值+1。游戏在进行了2101次操作(每个玩家进行了2100次操作)之后自动结束。R想要最小化结果的字典序,而Y想要最大化结果的字典序,在这种策略下,游戏结束后的s。

【思路】
字位在字典序中的权值:越靠前,权值越大。
操作后变小:z
操作后变大:a~y
设最高位的z为z1。
R要么结束,要么对z1进行操作。
Y要么结束,要么对权值比z1高(如果z1不存在,其权值为0)的权值最高的a~x进行操作。
为了不让Y有机会机会操作,z1之前不能有a~x,否则结束。在R操作之后,在R操作的位置进行操作为最佳策略,优于结束游戏。在一轮操作后,R操作的位置变为b,且必定在z1之前,所以R结束游戏。

#include<stdio.h>
#include<string.h>
int n;
char ch[11000];
void solve(){
	scanf("%s",ch+1);
	n=strlen(ch+1);
	int now=1;
	while (now<=n&&ch[now]=='y') now++;//找出第一个不是y的位置 
	if (now>n||ch[now]!='z')//第一个既不是y,又不是z
		printf("%s\n",ch+1);
	else{//第一个是z
		ch[now]='b';
		printf("%s\n",ch+1);
	}
}
int main(){
	int t; scanf("%d",&t);
	while(t--) solve();
	return 0;
}

【反思】本游戏其实在进行了一轮之后就结束了,题目描绘具有迷惑性。最优策略也包括减小对方的决策空间。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值