【项目介绍】
张梁是sp包的武将,其有一项技能“方统”,需要计算一系列牌的点数,使之和为36,然后便可对敌方造成大量伤害,具体信息可参考百科,想必点进来的都是懂张梁作为大数学家的含金量的😜,本项目就是为了研究一个算法,快速计算所有的炸人方案
什么是“大数学家”啊?
【代码展示】
引言:以大雁的这篇张梁视频跳转到1:00为例
张梁牌区所有点数为:2、12、13
“方”区所有点数为:6、7、12、12
一、函数介绍
- 全局变量
#define BOOM 36 //懂的都懂
// 牌区,空白处全为0
int PaiQ[20]={2,12,13};
// “方”区
int Fang[20]={6,7,12,12};
// 用于记录应当排除的“方”的下标,空白处赋为-1
int record[20];
int Fsum=0; //“方”区点数和
int Flen=0; //“方”区牌数
- 次要函数
/*将传入的数组初始化为value*/
void init(int* Array,int value)
{
for(int i=0;i<20;i++) Array[i]=value;
}
/*将数组Array视作队列压入新元素*/
void push(int* Array, int value, int newp)
{
// 令新元素newp替换第一个空白处(值为value)
for(int i=0;i<20;i++){
if(Array[i]==value){
Array[i]=newp;
break;
}
}
}
/*将数组Array视作队列弹出旧元素*/
void pop(int* Array,int oldp)
{
for(int i=0;i<20;i++){
// 找到与oldp同值的元素后,标记下标i
// 将后面所有元素依次往前推一位,直到遇到空白处
if(Array[i]==oldp){
for(;i<20;i++){
if(Array[i]==0) break;
Array[i]=Array[i+1];
}
break;
}
}
}
- 主要函数,各语句含义见注释
/*递归求解炸人方案,由总和倒推到单个元素(实际上推不到,最少必须有2张“方”才能炸人),
本质上是组合数的思想,fsum是子全“方”和,tag是即将减去的元素的下标,calC是计算组合数的意思*/
int calC(int fsum, int PaiQ_i, int tag){
fsum = fsum - Fang[tag];
// 这里与canBoom里判断的思想是一样的
if(fsum + PaiQ_i < BOOM) return 0;
else if(fsum + PaiQ_i == BOOM){
init(record,-1); //算出了炸人方案,表示要记录元素了,先初始化一下record
printf("可以炸人!方案为:");
return 1;
}
else if(fsum + PaiQ_i > BOOM){
while(++tag<Flen){ //++tag表示tag+1后的值,这里图省字了
if(calC(fsum, PaiQ_i, tag)==1){
// 因为我是递归地减去元素的,而要输出的是剩下的元素,
// 所以需要先记录每一步删去的是什么元素,再统一输出剩余的元素
push(record,-1,tag);
return 1;
}
}
}
}
/*输出解决方案*/
void printScheme(int PaiQ_i)
{
// 此时record里记录的是应当删去的元素下标
// 这里采取的方法是遍历record,将Fang(先用tmp复制)里对应下标的元素赋值为-1
// 然后遍历tmp,输出剩余元素
int tmp[20]={0};
memcpy(tmp,Fang,20);
for(int i=0;i<20;i++){
if(record[i]==-1) break;
tmp[record[i]]=-1;
}
for(int i=0;i<20;i++){
if(tmp[i]==-1) continue;
if(tmp[i]==0) break;
printf("%d ",tmp[i]);
}
printf("+ 点数为%d的牌。\n",PaiQ_i);
}
/*判断是否可以炸人*/
void canBoom(){
init(record,-1); //初始化记录表
// 计算“方”区的点数和
for(int i=0;i<20;i++){
if(Fang[i]==0) break;
Fsum += Fang[i];
Flen++;
}
// 遍历牌区,抽出每一张牌,判断是否能炸人
for(int i=0;i<20;i++){
if(PaiQ[i]==0) break; //遇到牌区空白处,停止遍历
// 以下对应三种情况:
// 1.全部“方”和该牌点数的和都不够36,就不必算“方”的组合情况了
// 2.全部“方”和该牌点数的和正好为36,直接输出,不必再算
// 3.全部“方”和该牌点数的和超过36,需算“方”的组合情况,循环使用递归函数
if(Fsum + PaiQ[i] < BOOM){
printf("所有“方”与点数为%d的牌的和不满36,无炸人解;_;\n",PaiQ[i]);
}
else if(Fsum + PaiQ[i] == BOOM){
printf("所有“方”与点数为%d的牌的和正好为36,刚好炸^0^\n",PaiQ[i]);
}
else if(Fsum + PaiQ[i] > BOOM){
int tag=0; //下标
boolean ret=FALSE; //结果标记
while(tag<Flen){ //从tag=0开始,传入sumC的是原tag,算完后tag+1
if(calC(Fsum, PaiQ[i], tag++)==1){
ret=TRUE; //表示可以炸人,不输入失败语句
push(record,-1,tag-1);
printScheme(PaiQ[i]);
init(record,-1);
continue; //算出炸人方案就不必再往下递归了,之后向同级或往上递归
}
}
if(!ret) printf("很遗憾,点数为%d的牌没有炸人方案:(\n",PaiQ[i]);
}
}
}
二、主程序
int main(){
int newcard=0;
printf("分析炸人方案中...\n");
canBoom();
printf("计算结束!\n");
return 0;
}
三、效果展示
与视频中所展示的方案一样
再以视频中1:48为例:
张梁牌区:1、2、6、12、13
“方”区:6、8、9、10
得出的结论一致,而且额外算出一解
四、结论与问题
这里主要用的算法是循环套递归,目的是模拟组合数的计算,张梁的炸人规则要求对牌区(设为有
n
1
n_1
n1张牌)的每一张牌进行判断,也就是循环
n
1
n_1
n1次;
然后对方区(设有
n
2
n_2
n2张牌)计算所有组合数,也就是计算
2
n
2^n
2n-1 次
牌区: n 1 n_1 n1
方区: C n 2 1 + C n 2 2 + C n 2 3 + . . . + C n 2 n 2 = 2 n 2 − 1 C_{n_2}^{1}+C_{n_2}^{2}+C_{n_2}^{3}+...+C_{n_2}^{n_2}=2^{n_2}-1 Cn21+Cn22+Cn23+...+Cn2n2=2n2−1
总共计算次数: n 1 × ( 2 n − 1 ) n_1\times(2^n-1) n1×(2n−1)
总的来说,根据规则,时间复杂度“必然”是要达到 O( 2 n 2^n 2n)的,甚至是指数 × \times ×线性,可见复杂度非常高,当然也可以根据规则减少一些枚举操作:
- 牌区的牌 + + +若干方的和 ≤ \leq ≤ 36,不必深入计算
- 方区最少要抽出2张方,不必考虑1张方(牌点数最大只有13,1张方太少)的情况。
- 值相等的牌避免多算、值相等的方避免重复组合
(比较麻烦,没有研究)
由于本人才疏学浅,不太清楚有没有解决这种穷举组合数问题更好的算法,不知是否有高手能在评论区予以指点?不过我自认为我这种计算方法应该是比较简单的,因为契合组合数的计算方式,基本没有对同一情况的重复计算。
其他问题还有就是不太方便,需要将已有的牌区牌和方区的点数全部输入程序再计算,只能应用于事后分析,不能用于实战,博主日后还会研究如何在实战中实时地展示炸人方法,也就是类似于插件那样的程序或app。