基于min-max搜索和alpha-beta(α-β)剪枝的五子棋的c语言实现(带简单禁手)

这实际上是我学校的C语言程序设计课结课作业。整个作业代码适中,算法难度合适,是对初学者很友好的一件结课作业。
对五子棋而言最重要的还是估值函数的选择,如果一开始就写出了一个全盘估值的算法,那么很快就能改完了。
我的实现是这么考虑的,对于一个棋盘,黑白两色分别考虑,对于一个颜色,选择棋盘上分数最大的一个点作为这个颜色棋盘的分数,而这个分数的选择是基于活二活三等的数量统计出来的。
然后将两个颜色做差得到分数。

int F(int x,int y,int col){//get F
	int ret=0;
	for(int dir=0;dir<4;++dir){
		int dx=DirX[dir];
		int dy=DirY[dir];
		int now=0;
		for(int i=-4;i<=4;++i){
			int nowx=x+i*dx;
			int nowy=y+i*dy;
			now|=Get_Val(nowx,nowy,col);
			now<<=2;
		}
		now>>=2;
		ret+=SCORE_V[now];
	}
	return ret;
}

在这里我对棋盘进行了hash映射,使用0、1、2代表空,己方有子,对方有子。因为一个棋子只需要考虑哈他四个方向计九个棋子的权值即可。
查找棋子分数我选择的是AC自动机实现。
在实现了AC自动机后,我又发现可能我们并不需要对每个局面都调用AC自动机,这个复杂度太高了,相反,我们可以用预处理的办法。
对于一个棋子,我认为在一条线上,其周围至多左四右四,合计9个棋子比较有用,他们只有黑白空三种情况,对一个子用2bit记录,只需要18个比特即可记录所有情况,这个总数是很少的,情况的总数只有区区262143种,可以直接先把这些情况权值全部算完再O(1)查询估值即可,这就是上文我说的hash映射,这使得估值的复杂度为至多16次位运算和四次访存。

#ifndef AC
#define AC
#define MAXLEN 10000 
    const int CHENG_5_SCORE = 5000000;
    const int HUO_4_SCORE = 100000;
    const int CHONG_4_SCORE = 10000;
    const int DAN_HUO_3_SCORE = 8000;
    const int TIAO_HUO_3_SCORE = 7000;
    const int MIAN_3_SCORE = 500;
    const int HUO_2_SCORE = 50;
    const int MIAN_2_SCORE = 10;
    char CHANG_LIAN[]="111111"; 
    char CHENG_5[] = "11111";
    char HUO_4[] = "011110";
    char DAN_HUO_3_1[] = "001110";
    char DAN_HUO_3_2[] = "011100";
    char TIAO_HUO_3_1[] = "010110";
    char TIAO_HUO_3_2[] = "011010";
    char HUO_2_1[] = "001100";
    char HUO_2_2[] = "010100";
    char HUO_2_3[] = "001010";
    char CHONG_4_1[] = "011112";
    char CHONG_4_2[] = "211110";
    char CHONG_4_3[] = "10111";
    char CHONG_4_4[] = "11101";
    char CHONG_4_5[] = "11011";
    char MIAN_3_1[] = "001112";
    char MIAN_3_2[] = "211100";
    char MIAN_3_3[] = "010112";
    char MIAN_3_4[] = "211010";
    char MIAN_3_5[] = "011012";
    char MIAN_3_6[] = "210110";
    char MIAN_3_7[] = "10011";
    char MIAN_3_8[] = "11001";
    char MIAN_3_9[] = "10101";
    char MIAN_3_10[] = "2011102";
    char MIAN_2_1[] = "000112";
    char MIAN_2_2[] = "211000";
    char MIAN_2_3[] = "001012";
    char MIAN_2_4[] = "210100";
    char MIAN_2_5[] = "010012";
    char MIAN_2_6[] = "210010";
    char MIAN_2_7[] = "10001";
    char SHUANGCHONG_4_1[]= "1011101";
    char SHUANGCHONG_4_2[]= "11011011";
    char SHUANGCHONG_4_3[]= "111010111";
//the template of the AC_AUTOMATION
struct AC_AUTO{
	int fail;
	int vis[4];
	int cnt;	
}T1[1001],T2[1001],T3[1001],T4[1001],T5[1001],T6[1001],T7[1001],T8[1001],T9[1001],T10[1001];
int ACcnt=0;
void build_T(char s[],struct AC_AUTO T[]){
	int len=strlen(s);
	int idx=0;
	for(int i=0;i<len;i++){
		if(!T[idx].vis[s[i]-'0']){
			T[idx].vis[s[i]-'0']=++ACcnt;
			idx=ACcnt; 
		}
		else{
			idx=T[idx].vis[s[i]-'0'];
		}
	}
	T[idx].cnt++;
}
void build_fail_Arr(struct AC_AUTO T[]){
	int q[MAXLEN]={};
	int head=0;
	int tail=-1;
	for(int i=0;i<3;i++){
		if(T[0].vis[i]){
			T[0].fail=0;
			q[++tail]=T[0].vis[i];
		}
	}
	while(head<=tail){
		int x=q[head];
		head++;
		for(int i=0;i<3;i++){
			if(T[x].vis[i]){
				T[T[x].vis[i]].fail=T[T[x].fail].vis[i];
				q[++tail]=T[x].vis[i]; 
			}
			else
				T[x].vis[i]=T[T[x].fail].vis[i];
		}
	}
} 
int AC_Quary(char s[],struct AC_AUTO T[]){
	int len=strlen(s);
	int idx=0;
	for(int i=0;i<len;i++){
		idx=T[idx].vis[s[i]-'0'];
		for(int j=idx;j&&T[j].cnt!=-1;j=T[j].fail){
			if(T[j].cnt>0)
				return 1;
		}
	}
	return 0;
}
//T1 MIAN_3 
//T2 HUO_2 
//T3_CHONG_4
//T4 TIAO_HUO_3
//T5 DAN_HUO_3
//T6 HUO_4
//T7 MIAN_2
//T8 CHENG_5 
void AC_Build(){
	build_T(MIAN_3_1,T1);
	build_T(MIAN_3_2,T1);
	build_T(MIAN_3_3,T1);
	build_T(MIAN_3_4,T1);
	build_T(MIAN_3_5,T1);
	build_T(MIAN_3_6,T1);
//	build_T(MIAN_3_7,T1);
//	build_T(MIAN_3_8,T1);
	build_T(MIAN_3_9,T1);
	build_T(MIAN_3_10,T1);
	ACcnt=0;
	build_fail_Arr(T1); 
	
	build_T(HUO_2_1,T2);
	build_T(HUO_2_2,T2);
	build_T(HUO_2_3,T2);
	ACcnt=0;
	build_fail_Arr(T2);
	
	build_T(CHONG_4_1,T3);
	build_T(CHONG_4_2,T3);
	build_T(CHONG_4_3,T3);
	build_T(CHONG_4_4,T3);
	build_T(CHONG_4_5,T3);
	ACcnt=0;
	
	build_fail_Arr(T3);	
	build_T(TIAO_HUO_3_1,T4);
	build_T(TIAO_HUO_3_2,T4);
	ACcnt=0;
	build_fail_Arr(T4);

	build_T(DAN_HUO_3_1,T5);
	build_T(DAN_HUO_3_2,T5);
	ACcnt=0;
	build_fail_Arr(T5); 
	
	build_T(HUO_4,T6);
	ACcnt=0;
	build_fail_Arr(T6);
	
	build_T(MIAN_2_1,T7);
	build_T(MIAN_2_2,T7);
	build_T(MIAN_2_3,T7);
	build_T(MIAN_2_4,T7);
	build_T(MIAN_2_5,T7);
	build_T(MIAN_2_6,T7);
	build_T(MIAN_2_7,T7);
	ACcnt=0;
	build_fail_Arr(T7);
	
	build_T(CHENG_5,T8);
	ACcnt=0;
	build_fail_Arr(T8);
	
	build_T(CHANG_LIAN,T9);
	ACcnt=0;
	build_fail_Arr(T9);

	build_T(SHUANGCHONG_4_1,T10);
	build_T(SHUANGCHONG_4_2,T10);
	build_T(SHUANGCHONG_4_3,T10);
	ACcnt=0;
	build_fail_Arr(T10);
}
#endif 

预处理模块:

int GETSCORE(char s[]){
	int ret=0;
	if(AC_Quary(s,T8))ret+=CHENG_5_SCORE;
	if(AC_Quary(s,T7))ret+=MIAN_2_SCORE;
	if(AC_Quary(s,T6))ret+=HUO_4_SCORE;
	if(AC_Quary(s,T5))ret+=DAN_HUO_3_SCORE;
	if(AC_Quary(s,T4))ret+=TIAO_HUO_3_SCORE;
	if(AC_Quary(s,T3))ret+=CHONG_4_SCORE;
	if(AC_Quary(s,T2))ret+=HUO_2_SCORE;
	if(AC_Quary(s,T1))ret+=MIAN_3_SCORE;		
	return ret;
}
void SCORE_init(){//prepare for the F
	for(int i=0;i<=262143;i++){
		int x=i;
		char s[10];
		int flag=1;
		for(int i=0;i<9;++i){
			int now=x&3;
			if(now==3)flag=0;
			x>>=2;
		}
		if(flag==0)continue;
		x=i;
		for(int i=0;i<9;++i){
			int now=x&3;
			if(now==0)s[i]='0';
			if(now==1)s[i]='1';
			if(now==2)s[i]='2';
			x>>=2;
		}
		s[9]='\0';
		SCORE_V[i]=GETSCORE(s);
		CHANGLIAN[i]=AC_Quary(s,T9);
		CHENG5[i]=AC_Quary(s,T8);
		CHONG4[i]=((AC_Quary(s,T6)+AC_Quary(s,T3))>=1);
		HUO3[i]=((AC_Quary(s,T5)+AC_Quary(s,T4))>=1); 
		SHUANGCHONG4[i]=AC_Quary(s,T10);
	}
}

如是,我们就得到了一个可以搜索一层,估值全盘的五子棋。
随后将其放置在min-max搜索上。
关于min-max搜索无须多叙,参考wiki的伪代码便可以轻松实现(前提是你有一个估值全盘的估值函数)

function  minimax(node, depth, maximizingPlayer) is
    if depth = 0 or node is a terminal node then
        return the heuristic value of node
    if maximizingPlayer then
        value := −∞
        for each child of node do
            value := max(value, minimax(child, depth − 1, FALSE))
        return value
    else (* minimizing player *)
        value := +for each child of node do
            value := min(value, minimax(child, depth − 1, TRUE))
        return value

另外,我发现在很多时候,前一步搜索结果可以被继承(即你下的那一步和对手下的那一步全部被搜索),因此我设计了一个树状链表记录搜索树。
关于禁手:
我的代码并没有设计需要迭代的复杂禁手判断,这主要的原因是为了增加搜索层数,普通禁手可以判断长连,双四,双三等最主要的禁手。依旧使用预处理思想尽快判断禁手,复杂度为稳定的48次运算。

int JudgeBan(int x,int y){//judge a point  
	int ret=0;//0 not ban 1 ban
	int cntCHANGLIAN=0;
	int cntCHENG5=0;
	int cntCHONG4=0;
	int cntSHUANGCHONG4=0;
	int cntHUO3=0; 
	for(int dir=0;dir<4;++dir){
		int dx=DirX[dir];
		int dy=DirY[dir];
		int now=0;
		for(int i=-4;i<=4;++i){
			int nowx=x+i*dx;
			int nowy=y+i*dy;
			now|=Get_Val(nowx,nowy,0);
			now<<=2;
		}
		now>>=2; 
		if(CHANGLIAN[now]){
			cntCHANGLIAN++;
		}
		else{
			if(CHENG5[now]){
				cntCHENG5++;
			}
			else{
				if(CHONG4[now]){
					cntCHONG4++;
				}
				else{
					if(HUO3[now]){
						cntHUO3++;
					}
				}
			}
		}
		if(SHUANGCHONG4[now]){
			cntSHUANGCHONG4++;
		}
	}	
	if(cntCHANGLIAN)return 1;
	if(cntCHENG5)return 0;
	return ((cntHUO3>=2)||(cntCHONG4>=2)||(cntSHUANGCHONG4>=1));
}

一些值得注意的点:

  1. 编译优化很重要,如果使用的VS,released版本就很不错。
  2. 电脑配置也很重要,特别是在一场淘汰赛中取得较好成绩,在使用Core i7 gen 8时单核频率3.0Ghz,这份代码只能跑13层每层9个点,但在使用Core i9 gen 12时单核频率4.5Ghz,这份代码就可以跑13层每层均摊10.5个点了。

最终这份代码得到了最后的冠军(并列第一,因为我和对手都能持黑击败对方,但事实上我的代码持黑可以用更少步数击败对手)
核心代码wzq.c:已附加基本的注释

#include<stdio.h>
#include<math.h> 
#include<string.h>
#include<stdlib.h>
#include<time.h> 
#include "opt.h"
#include "AC.h"
#include "IO.h"
#include "algorithm.h"
#include "opt.h" 
#include "SCORE.h"
#include "tree.h" 
//#include "heap.h"
//#define Debug 0
//#define Debug2 0
#define depth 10
#define SUM 10
int T=0;
int nowrate=0;

int Limit1[13]={9,9,9,9,9,9,9,9,9,9,9,9,9}; 
//int Limit1[13]={10,9,9,9,9,8,8,8,8,8,8,8,8}; 
int Limit2[13]={9,9,9,9,9,8,8,8,8,8,8,8,8};
int Limit3[13]={8,8,8,7,7,7,7,7,6,6,6,6,6};
int (*Limit)[13];
struct Node{
	int x,y;
	int sum;
};
void putchess(int x,int y,int cur);
void UpdateV(int x,int y,int player);//update the value of the chessboard
void GetBan();
//
void player_to_player();
void computer_to_player(); 
void AC_Build();//build AC_AUTOMACHINE
void SCORE_init();
void pvalue();//print value  for debug
int checkwin();
void Tree_DFS(struct Tree_Node * now);//free the unnecessary tree node  
void regret();//regret two steps 
int main(){
	srand(233);
	printf("1:player to player ,2:player to computer \n");
	int x;
	scanf("%d",&x);
	if(x==1){
		player_to_player();
	}
	else{
		computer_to_player();		
	}
	return 0;
}
void player_to_player(){
	InitBoardArray();
	AC_Build();
	SCORE_init();
	DisplayBoard();
	for(int cur=0;;cur^=1){
		char c[40];
		int x,y;
		while(1){
			scanf("%s",c);
			int len=strlen(c);
			if(len==5){
				if(c[0]=='p'&&c[1]=='r'&&c[2]=='i'&&c[3]=='n'&&c[4]=='t'){
					print_step();
				}
				continue;
			}
			if(len==4){
				if(c[0]=='q'&&c[1]=='u'&&c[2]=='i'&&c[3]=='t'){
					return;
				}
			}
			if(len<=3&&len>1){
				int a;
				char b;
				b=(GETY(c));
				a=(GETX(c));
				y=(GETY(c))-'a';
				x=SIZE-(GETX(c));
				if(a==0||b=='z'||B.Array[x][y]!=2){
					printf("error input\n");
				}		
				else{
					break;
				}		
			}
			else{
				printf("error input\n");
			}	
		}
		y=(GETY(c))-'a';
		x=SIZE-(GETX(c));
		putchess(x,y,cur);
		GetBan();
		DisplayBoard();
		int check=checkwin();
		if(check!=2){
			if(check==0)
				puts("Black Win");
			else puts("White Win");
			return ;
		}
	}	
}
//
int F(int x,int y,int player);//judge function
int CheckCHENG_5(int x,int y,int col);
struct Node GetNext(int alpha,int beta,int player,int dep,struct Tree_Node *now);

void computer_to_player(){
	//0 black 1 white
	puts("1 you first,0 computer first");
	InitBoardArray();
	AC_Build();
	SCORE_init();
	int player;
	scanf("%d",&player);
	Limit=Limit3;
	int cur=1;
	if(player==0){//computer first, put the chess at H8
		cur^=1;
		putchess(7,7,cur);
		DisplayBoard();
	}
	if(player==1){
		DisplayBoard();//player first, giving a blank board
	}
	struct Tree_Node *head;
	head=malloc(sizeof(struct Tree_Node));
	memset(head,0,sizeof(struct Tree_Node));
	for(;;){
		if(B.cnt>=4){
			Limit=Limit1;
		}
		char c[40];
		int x,y;
		while(1){
			scanf("%s",c);
			int len=strlen(c);
			if(len==1){
				if(c[0]=='p'){//print the value of the board
					pvalue();
				}
				continue; 
			}
			if(len==6){
				if(c[0]=='r'&&c[1]=='e'&&c[2]=='g'&&c[3]=='r'&&c[4]=='e'&&c[5]=='t');{//regret,and give a new node to the tree.
					regret();
					Tree_DFS(head);
					head=malloc(sizeof(struct Tree_Node));
					memset(head,0,sizeof(struct Tree_Node));					
				} 
			}
			if(len==5){
				if(c[0]=='p'&&c[1]=='r'&&c[2]=='i'&&c[3]=='n'&&c[4]=='t'){
					print_step();
				}
				continue;
			}
			if(len==4){
				if(c[0]=='q'&&c[1]=='u'&&c[2]=='i'&&c[3]=='t'){
					return;
				}
			}
			if(len<=3&&len>1){
				int a;
				char b;
				b=(GETY(c));
				a=(GETX(c));
				y=(GETY(c))-'a';
				x=SIZE-(GETX(c));
				if(a==0||b=='z'||B.Array[x][y]!=2){
					printf("error input\n");
				}		
				else{
					break;
				}		
			}
			else{
				printf("error input\n");
			}	
		}
		y=(GETY(c))-'a';
		x=SIZE-(GETX(c));
		cur^=1;	
		putchess(x,y,cur);
		GetBan();
		DisplayBoard();
		int check=checkwin();
		if(check!=2){
			if(check==0)
				puts("Black Win");
			else puts("White Win");
			return ;
		}
		int findson=0;
		struct Tree_Node * tmpnow=head;
		//if already find some sons, just turn the head to that son.
		if(head->flag)
			for(int i=1;i<=head->cnt;++i){
				if(head->son[i]!=NULL&&head->son[i]->x==x&&head->son[i]->y==y){
					tmpnow=head->son[i];
					findson=1;
				}
				else{
					if(head->son[i]!=NULL){
						Tree_DFS(head->son[i]);
					}
				}
			}
		head=tmpnow;
		//not find the son, just create a new tree
		if(!findson){			
			head=malloc(sizeof(struct Tree_Node));
			memset(head,0,sizeof(struct Tree_Node));
		}
		//print the pos and predicting step
		if(findson){
			printf("%p\n",head);
			for(int i=1;i<=head->cnt;++i){
				printf("%p %d %c\n",head->son[i],15-head->son[i]->x,'A'+head->son[i]->y);
			} 
		}
		struct Node tmp;
		T=clock();
		tmp=GetNext(-1e9,1e9,player,depth,head);	
		cur^=1;
		#ifdef Debug2
			printf("%d %d %d\n",tmp.x,tmp.y,tmp.sum);
		#endif
		nowrate=tmp.sum;
		putchess(tmp.x,tmp.y,cur);
		tmpnow=head;
		for(int i=1;i<=head->cnt;++i){
			if(head->son[i]!=NULL&&head->son[i]->x==tmp.x&&head->son[i]->y==tmp.y){
				tmpnow=head->son[i];
				puts("IN");
			}
			else{
				if(head->son[i]!=NULL){
					Tree_DFS(head->son[i]);
				}
			}
		}		
		head=tmpnow;
		GetBan();
		DisplayBoard();
		printf("%d %c %d ms\n",15-tmp.x,'A'+tmp.y,(clock()-T)/1000);
		check=checkwin();
		if(check!=2){
			if(check==0)
				puts("Black Win");
			else puts("White Win");
			return ;
		}
	}
}
/*
function  minimax(node, depth, maximizingPlayer) is
    if depth = 0 or node is a terminal node then
        return the heuristic value of node
    if maximizingPlayer then
        value := -INF
        for each child of node do
            value := max(value, minimax(child, depth ? 1, FALSE))
        return value
    else (* minimizing player *)
        value := +INF
        for each child of node do
            value := min(value, minimax(child, depth ? 1, TRUE))
        return value
*/
void UpdateV(int x,int y,int col){
	if(B.Array[x][y]!=2)val[x][y]=F(x,y,B.Array[x][y]);
	else{val[x][y]=0;}
	for(int dir=0;dir<4;++dir){
		int dx=DirX[dir];
		int dy=DirY[dir];
		for(int i=-4;i<=4;i++){
			if(i==0)continue;
			int nowx=x+dx*i;
			int nowy=y+dy*i;
			if(inX(nowx)&&inY(nowy)&&B.Array[nowx][nowy]!=2){
				val[nowx][nowy]=F(nowx,nowy,B.Array[nowx][nowy]);
			}
		}
	}
}
struct Node GetNext(int alpha,int beta,int player,int dep,struct Tree_Node* nowNode){
	struct Node ret={-1,-1,-1e9}; 
	// if((T-clock())/1000>5000&&B.cnt>=6)Limit=Limit2;
	int cnt=0;
	if(dep==0){
		int now=-1e9;
		for(int i=0;i<SIZE;i++){
			for(int j=0;j<SIZE;j++){
				int sum=0;
				if(B.Array[i][j]==2){//the board is blank, put the chess
					B.Array[i][j]=player;
					int Mx=F(i,j,player);
					B.Array[i][j]=(player^1);
					int Mn=F(i,j,(player^1));	
					B.Array[i][j]=2;
					sum=Mx+Mn;//calculating the value of the baord
					/*
					using insert sort to sort the board and find the Nth maxist to search
					*/ 
					if(cnt<SUM-1){
						cnt++;
						nowNode->son[cnt]=malloc(sizeof(struct Tree_Node));
						memset(nowNode->son[cnt],0,sizeof(struct Tree_Node));
						nowNode->son[cnt]->x=i;
						nowNode->son[cnt]->y=j;
						nowNode->son[cnt]->sum=sum;
						for(int i=cnt-1;i>=1;i--){
							if((nowNode->son[i]->sum)<(nowNode->son[i+1]->sum)){
								struct Tree_Node* tmp=nowNode->son[i+1];
								nowNode->son[i+1]=nowNode->son[i];
								nowNode->son[i]=tmp;
							}
						}
					}
					else{
						if(nowNode->son[cnt]->sum<sum){
							memset(nowNode->son[cnt],0,sizeof(struct Tree_Node));
							nowNode->son[cnt]->x=i;
							nowNode->son[cnt]->y=j;
							nowNode->son[cnt]->sum=sum;
							for(int i=cnt-1;i>=1;i--){
								if((nowNode->son[i]->sum)<(nowNode->son[i+1]->sum)){
									struct Tree_Node* tmp=nowNode->son[i+1];
									nowNode->son[i+1]=nowNode->son[i];
									nowNode->son[i]=tmp;
								}						
							}
						}
					}
				}		
			}	
		}
		nowNode->cnt=cnt;
		if((player==0&&(dep%2==0))||(player==1&&(dep%2==1))){//get the ban of the black goal
			for(int i=1;i<=cnt;i++){
				nowNode->son[i]->ban=checkBan(nowNode->son[i]->x,nowNode->son[i]->y);
			}	
		}		
		nowNode->flag=1;
		int tot=0; 
		for(int i=1;i<=nowNode->cnt;i++){
			if(nowNode->son[i]==NULL)continue; 
			++tot;
			if(tot>(*Limit)[depth-dep])break;
			if(((player==0&&(dep%2==0))||(player==1&&(dep%2==1)))&&nowNode->son[i]->ban){
					continue;
			}
			int sum=0;
			int x=nowNode->son[i]->x;
			int y=nowNode->son[i]->y;
			int nowcol=(dep%2==1)?(player^1):player;
			int Mx=-1e9;
			int Mn=-1e9;
			if(CheckCHENG_5(x,y,nowcol)){
				sum=5000000;
			}
			else{
				B.Array[x][y]=nowcol;
				UpdateV(x,y,nowcol);
				for(int u=0;u<SIZE;u++){
					for(int v=0;v<SIZE;v++){
						if(B.Array[u][v]==nowcol){
							Mx=max(Mx,val[u][v]);
						}
						if(B.Array[u][v]==(nowcol^1)){
							Mn=max(Mn,val[u][v]); 
						}
					}
				}
				B.Array[x][y]=2;
				UpdateV(x,y,nowcol);
				sum=Mx-((nowcol==1&&nowrate<=2000)?4:1)*Mn;		
								
			}
			if(sum>now){
				now=sum;
				ret.x=x;
				ret.y=y;
				ret.sum=sum;
			}			
		} 
		return ret;
	}
	if(B.cnt==1&&dep==depth){
		for(int dir=0;dir<8;++dir){
			int x=B.curx+DirX[dir];
			int y=B.cury+DirY[dir];
			if(inX(x)&&inY(y)&&B.Array[x][y]==2){
				cnt++;
				nowNode->son[cnt]=malloc(sizeof(struct Tree_Node));
				memset(nowNode->son[cnt],0,sizeof(struct Tree_Node));
				nowNode->son[cnt]->x=x;
				nowNode->son[cnt]->y=y;
			}
		}
		nowNode->cnt=cnt;
		nowNode->flag=1;
	}
	else{
		if((nowNode->flag)!=1){
			int nowcol=(dep%2==1)?(player^1):player;
			for(int i=0;i<SIZE;i++){
				for(int j=0;j<SIZE;j++){
					int sum=0;
					if(B.Array[i][j]==2){
						B.Array[i][j]=player;
						int Mx=F(i,j,player);
						B.Array[i][j]=(player^1);
						int Mn=F(i,j,(player^1));	
						B.Array[i][j]=2;
						sum=Mx+Mn; 
						if(cnt<SUM-1){
							cnt++;
							nowNode->son[cnt]=malloc(sizeof(struct Tree_Node));
							memset(nowNode->son[cnt],0,sizeof(struct Tree_Node));
							nowNode->son[cnt]->x=i;
							nowNode->son[cnt]->y=j;
							nowNode->son[cnt]->sum=sum;
							for(int i=cnt-1;i>=1;i--){
								if((nowNode->son[i]->sum)<(nowNode->son[i+1]->sum)){
									struct Tree_Node* tmp=nowNode->son[i+1];
									nowNode->son[i+1]=nowNode->son[i];
									nowNode->son[i]=tmp;
								}
							}
						}
						else{
							if(nowNode->son[cnt]->sum<sum){
								memset(nowNode->son[cnt],0,sizeof(struct Tree_Node));
								nowNode->son[cnt]->x=i;
								nowNode->son[cnt]->y=j;
								nowNode->son[cnt]->sum=sum;
								for(int i=cnt-1;i>=1;i--){
									if((nowNode->son[i]->sum)<(nowNode->son[i+1]->sum)){
										struct Tree_Node* tmp=nowNode->son[i+1];
										nowNode->son[i+1]=nowNode->son[i];
										nowNode->son[i]=tmp;
									}						
								}
							}
						}
					}
				}		
			}
			nowNode->cnt=cnt;
			if((player==0&&(dep%2==0))||(player==1&&(dep%2==1))){
				for(int i=1;i<=cnt;i++){
					nowNode->son[i]->ban=checkBan(nowNode->son[i]->x,nowNode->son[i]->y);
				}	
			}
			nowNode->flag=1; 
		}
	}
	//doing alpha and beta search
	if(dep%2==1){
		ret.sum=beta;
	} 
	else ret.sum=alpha;
	int tot=0;
	for(int i=1;i<=nowNode->cnt;i++){
		if(nowNode->son[i]==NULL)continue; 
		++tot;
		if(tot>(*Limit)[depth-dep])break;
		int x=nowNode->son[i]->x;
		int y=nowNode->son[i]->y;
		if(((player==0&&(dep%2==0))||(player==1&&(dep%2==1)))&&nowNode->son[i]->ban){
				continue;
		}
		if(dep%2==0){//max node
			B.Array[x][y]=player;
			UpdateV(x,y,player);
			if(CheckCHENG_5(x,y,player)){
					ret.x=x;
					ret.y=y;
					ret.sum=5000000;
					alpha=5000000;
			}
			else{
				struct Node now=GetNext(alpha,beta,player,dep-1,nowNode->son[i]);
				if(now.sum>ret.sum){
					ret.x=x;
					ret.y=y;
					ret.sum=now.sum;
					alpha=now.sum;
				}	
				if(dep==depth){
					printf("%d %c sum=%d\n",15-x,(char)('A'+y),now.sum);
				}					
			}
			B.Array[x][y]=2;
			UpdateV(x,y,player);
			if(beta<=alpha)break;
		}
		else{//min node
			B.Array[x][y]=(1-player);
			UpdateV(x,y,(player^1));
			if(CheckCHENG_5(x,y,1-player)){
				ret.x=x;
				ret.y=y;
				ret.sum=-5000000;
				beta=-5000000;
			}
			else{
				struct Node now=GetNext(alpha,beta,player,dep-1,nowNode->son[i]);
				if(now.sum<ret.sum){
					ret.x=x;
					ret.y=y;
					ret.sum=now.sum;
					beta=now.sum;
				}						
			}
			B.Array[x][y]=2;
			UpdateV(x,y,(player^1));
			if(beta<=alpha)break;
		}				
	}
	return ret;
}
void Tree_DFS(struct Tree_Node * now){//free the tree node
	if(now==NULL)return;
	for(int i=1;i<=now->cnt;i++){
		if(now->son[i]!=NULL){
			Tree_DFS(now->son[i]);
		}
	}
	free(now);
}
int GETSCORE(char s[]){
	int ret=0;
	if(AC_Quary(s,T8))ret+=CHENG_5_SCORE;
	if(AC_Quary(s,T7))ret+=MIAN_2_SCORE;
	if(AC_Quary(s,T6))ret+=HUO_4_SCORE;
	if(AC_Quary(s,T5))ret+=DAN_HUO_3_SCORE;
	if(AC_Quary(s,T4))ret+=TIAO_HUO_3_SCORE;
	if(AC_Quary(s,T3))ret+=CHONG_4_SCORE;
	if(AC_Quary(s,T2))ret+=HUO_2_SCORE;
	if(AC_Quary(s,T1))ret+=MIAN_3_SCORE;		
	return ret;
}
void SCORE_init(){//prepare for the F
	for(int i=0;i<=262143;i++){
		int x=i;
		char s[10];
		int flag=1;
		for(int i=0;i<9;++i){
			int now=x&3;
			if(now==3)flag=0;
			x>>=2;
		}
		if(flag==0)continue;
		x=i;
		for(int i=0;i<9;++i){
			int now=x&3;
			if(now==0)s[i]='0';
			if(now==1)s[i]='1';
			if(now==2)s[i]='2';
			x>>=2;
		}
		s[9]='\0';
		SCORE_V[i]=GETSCORE(s);
		CHANGLIAN[i]=AC_Quary(s,T9);
		CHENG5[i]=AC_Quary(s,T8);
		CHONG4[i]=((AC_Quary(s,T6)+AC_Quary(s,T3))>=1);
		HUO3[i]=((AC_Quary(s,T5)+AC_Quary(s,T4))>=1); 
		SHUANGCHONG4[i]=AC_Quary(s,T10);
	}
}
inline int Get_Val(int x,int y,int col){//the same col return 1, blank point return 0 other return 2
	if(!inX(x)||!inY(y))return 2;
	if(B.Array[x][y]==2)return 0;
	return B.Array[x][y]==col?1:2;
}
int F(int x,int y,int col){//get F
	int ret=0;
	for(int dir=0;dir<4;++dir){
		int dx=DirX[dir];
		int dy=DirY[dir];
		int now=0;
		for(int i=-4;i<=4;++i){
			int nowx=x+i*dx;
			int nowy=y+i*dy;
			now|=Get_Val(nowx,nowy,col);
			now<<=2;
		}
		now>>=2;
		ret+=SCORE_V[now];
	}
	return ret;
}
int CheckCHENG_5(int x,int y,int col){
	// 1 col 0 blank - other 
	for(int dir=0;dir<4;++dir){
		int dx=DirX[dir];
		int dy=DirY[dir];
		int now=0;
		for(int i=-4;i<=4;++i){
			if(i==0){
				now|=1;
				now<<=2;
				continue;
			}
			int nowx=x+i*dx;
			int nowy=y+i*dy;
			now|=Get_Val(nowx,nowy,col);
			now<<=2;
		}
		now>>=2;		
		if(CHENG5[now])return 1;
	}
	return 0;
}
int JudgeBan(int x,int y){//judge a point  
	int ret=0;//0 not ban 1 ban
	int cntCHANGLIAN=0;
	int cntCHENG5=0;
	int cntCHONG4=0;
	int cntSHUANGCHONG4=0;
	int cntHUO3=0; 
	for(int dir=0;dir<4;++dir){
		int dx=DirX[dir];
		int dy=DirY[dir];
		int now=0;
		for(int i=-4;i<=4;++i){
			int nowx=x+i*dx;
			int nowy=y+i*dy;
			now|=Get_Val(nowx,nowy,0);
			now<<=2;
		}
		now>>=2; 
		if(CHANGLIAN[now]){
			cntCHANGLIAN++;
		}
		else{
			if(CHENG5[now]){
				cntCHENG5++;
			}
			else{
				if(CHONG4[now]){
					cntCHONG4++;
				}
				else{
					if(HUO3[now]){
						cntHUO3++;
					}
				}
			}
		}
		if(SHUANGCHONG4[now]){
			cntSHUANGCHONG4++;
		}
	}	
	if(cntCHANGLIAN)return 1;
	if(cntCHENG5)return 0;
	return ((cntHUO3>=2)||(cntCHONG4>=2)||(cntSHUANGCHONG4>=1));
}
int checkBan(int x,int y){
	B.Array[x][y]=0;
	if(JudgeBan(x,y)){
		B.Array[x][y]=2;		
		return 1;
	}
	B.Array[x][y]=2;
	return 0;
}
int checkwin(){//check the whether winner exists
	for(int i=0;i<SIZE;i++){
		for(int j=0;j<SIZE;j++){
			if(B.Array[i][j]!=2){
				int flag=0;
				for(int dir=0;dir<=3;++dir){
					int dx=i,dy=j;
					for(int k=1;k<=4;++k){
						dx+=DirX[dir];
						dy+=DirY[dir];
						if(!inX(dx)||!inY(dy))break;
						if(B.Array[dx][dy]!=B.Array[i][j])break;
						if(k==4)flag=1;
					}
					if(flag==1)break;
				}
				if(flag==1)return B.Array[i][j];
			}
		}
	}
	return 2;
} 
void putchess(int x,int y,int cur){
	B.cntstep[++B.cnt][0]=x;
	B.cntstep[B.cnt][1]=y;
	B.curx=x;
	B.cury=y;
	B.Array[x][y]=cur;
	UpdateV(x,y,cur);
}
//printf the latest value
void pvalue(){
	for(int i=0;i<SIZE;i++){
		printf("%2d ",i);
		for(int j=0;j<SIZE;j++){
			printf("%d ",val[i][j]);
		}
		puts("");
	}
	for (char ary='A';ary<'A'+SIZE;ary++)
		printf("%c ",ary);
	printf("\n");
}
//Get the banned point
void GetBan(){
	memset(Ban,0,sizeof(Ban));
	for(int i=0;i<SIZE;++i){
		for(int j=0;j<SIZE;++j){
			if(B.Array[i][j]==2){
				Ban[i][j]=checkBan(i,j);
			}
		}
	}
}
//delete the latest two steps and refresh the banned point
void regret(){
	B.Array[B.cntstep[B.cnt][0]][B.cntstep[B.cnt][1]]=2;
	UpdateV(B.cntstep[B.cnt][0],B.cntstep[B.cnt][1],2);
	B.cnt--;
	B.Array[B.cntstep[B.cnt][0]][B.cntstep[B.cnt][1]]=2;
	UpdateV(B.cntstep[B.cnt][0],B.cntstep[B.cnt][1],2);
	B.cnt--;
	GetBan();
	DisplayBoard();	
}

tree.h

#ifndef _Tree_
#define _Tree_
struct Tree_Node{
	int x,y,flag,sum,ban,cnt;
	struct Tree_Node* son[11];
};
#endif 

heap.h

#ifndef heap
#define heap 
struct Heap{
	struct HeapNode{
		int x;
		int y;
		int sum;
	}h[10];
	int heapcnt;	
};
void push(struct HeapNode t,struct HeapNode h[], int *heapcnt){
	(*heapcnt)++;
	int now=*heapcnt;
	h[*heapcnt]=t;
	while(now>1&&h[now].sum<h[now>>1].sum){
		swap(h[now],h[now>>1]);
		now=now>>1;
	}
}
void pop(struct HeapNode h[],int *heapcnt){
	h[1]=h[*heapcnt];
	(*heapcnt)--;
	int now=1;
	int son;
	while(now*2<=*heapcnt){
		son=now*2;
		if(son+1<=*heapcnt&&h[son].sum>h[son+1].sum){
			son++;
		}
		if(h[now].sum<h[son].sum)
			return;
		swap(h[son],h[now]);
		now=son;
	}
}
struct HeapNode top(struct HeapNode h[]){
	return h[1];
}
#endif

增加编译速度的opt.h

#pragma once
#ifdef __GNUC__
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#endif

交互库IO.h

#ifndef IO
#define IO
#define SIZE 15
#define MAXSTEP 15*15+1
struct{
	int cntstep[MAXSTEP][2];
	int Array[SIZE][SIZE];//0 black 1 white 2 blank	
	int cnt,curx,cury;
}B;
int DirX[8]={-1,0,1,1,1,0,-1,-1};
int DirY[8]={1,1,1,0,-1,-1,-1,0};
int val[SIZE][SIZE];
int val0[SIZE][SIZE];
int val1[SIZE][SIZE];
int Board[SIZE][SIZE]={};
int Ban[SIZE][SIZE];
int neighbor[SIZE][SIZE]={};
void print_int(int x,int y);//print x y for debug
void print_board(); 
void print_step(); 
int GETX(char c[]);
char GETY(char c[]);
void InitBoardArray(); 
void DisplayBoard();	
void Bclear();	
void Bclear(){//initial the board
	B.cnt=B.curx=B.cury=0;
	memset(B.cntstep,0,sizeof(B.cntstep));
	for(int i=0;i<SIZE;++i){
		for(int j=0;j<SIZE;++j){
			B.Array[i][j]=2;
		}
	}
}
void InitBoardArray(){
	Bclear(); 
	Board[0][0]=1;
	Board[0][SIZE-1]=2;
	Board[SIZE-1][SIZE-1]=3;
	Board[SIZE-1][0]=4;
	for(int j=1;j<=SIZE-2;j++){
		Board[j][0] = 5;
	}
	for(int i=1;i<=SIZE-2;i++){
		Board[0][i] = 6;
	}
	for(int j=1;j<=SIZE-2;j++){
		Board[j][SIZE-1]=7;
	}
	for(int i=1;i<=SIZE-2;i++){
		Board[SIZE-1][i]=8;
	}
	for(int j=1;j<=SIZE-2;j++){
		for(int i=1;i<=SIZE-2;i++){
			Board[j][i] = 9;
		}
	}
	for(int i=0;i<SIZE;i++){
		for(int j=0;j<SIZE;j++){
			val[i][j]=0;
		}
	}
}

void DisplayBoard(){	
	char line;
	#ifdef __linux__ 
		system("clear");
	#endif
	#ifdef _WIN64
		system("clr");
	#endif
	puts("laujiyeung");
	for(int j = 0,line = 15; j <= SIZE - 1; j++){
		printf("%2d",line);
		line -= 1;
		for(int i = 0; i <= SIZE - 1; i++){
			if(Ban[j][i]){
				printf("× ");
			}
			else 
				if(B.Array[j][i]!=2){
					if(B.curx==j&&B.cury==i){
						switch(B.Array[j][i]){
							case 0:
								printf("▲");
								break;
							case 1:
								printf("△");
								break;
						}
					} 
					else{
						switch(B.Array[j][i]){
							case 0:
								printf("●");
								break;
							case 1:
								printf("◎");
								break;
						}
					}
					#ifdef __linux__
						if(i!=14) printf("─");
					#endif				
				}
				else{
					switch(Board[j][i]){
					case 1:
						printf("┌");
						break;
						
					case 2:
						printf("┐");
						break;
						
					case 3:
						printf("┘");
						break;
						
					case 4:
						printf("└");
						break;
						
					case 5:
						printf("├");
						break;
						
					case 6:
						printf("┬");
						break;
						
					case 7:
						printf("┤");
						break;
						
					case 8:
						printf("┴");
						break;
						
					case 9:
						printf("┼");
						break;
					}
					if(i!=14) printf("─");		
				}
			if(i == SIZE - 1){
				printf("\n");
			}
		}
	}
	printf("  ");
	for (char ary='A';ary<'A'+SIZE;ary++)
		printf("%c ",ary);
	printf("\n");
}
void print_int(int x,int y){
	printf("%d %d\n",x,y);
}
void print_board(){
	for(int i=0;i<SIZE;++i){
		for(int j=0;j<SIZE;++j){
			printf("%d ",B.Array[i][j]);
		}
		puts("");
	}
} 
int GETX(char c[]){
	int x=0;
	int pos=0;
	int len=strlen(c);
	while((c[pos]<'0'||c[pos]>'9')&&pos<len)
		pos++;
	while(c[pos]>='0'&&c[pos]<='9'&&pos<len){
		x=x*10+c[pos]-'0';
		pos++;
	}
	return x;
}
char GETY(char c[]){
	char x='z';
	int pos=0;
	int len=strlen(c);
	while((c[pos]<'a'||c[pos]>'o')&&pos<len)
		pos++;
	x=c[pos];
	return x;
}
void print_step(){
	for(int i=1;i<=B.cnt;i++){
		printf("%s %d %c\n",(i%2?"black":"white"),SIZE-B.cntstep[i][0],(char)('A'+B.cntstep[i][1]));
	}
}
#endif

基本算法库:

#ifndef algorithm
#define algorithm
#define min(x,y) ({            \
    int __min1 = (x);          \
    int __min2 = (y);          \
    __min1 < __min2 ? __min1 : __min2; })
 
#define max(x,y) ({            \
    int __max1 = (x);          \
    int __max2 = (y);          \
    __max1 > __max2 ? __max1 : __max2; })
    
#define swap(x,y)   {struct Node t;t=x;x=y;y=t;}

#define inX(x) ((x)>=0&&(x)<SIZE)
#define inY(y) ((y)>=0&&(y)<SIZE) 

//#define Get_Val(x,y,col)({		   	\
//	int _x=(x);					\
//	int _y=(y);					\
//	(!in(_x)||())
//})
#endif  
  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
======================================================================== MICROSOFT FOUNDATION CLASS LIBRARY : fir ======================================================================== AppWizard has created this fir application for you. This application not only demonstrates the basics of using the Microsoft Foundation classes but is also a starting point for writing your application. This file contains a summary of what you will find in each of the files that make up your fir application. fir.dsp This file (the project file) contains information at the project level and is used to build a single project or subproject. Other users can share the project (.dsp) file, but they should export the makefiles locally. fir.h This is the main header file for the application. It includes other project specific headers (including Resource.h) and declares the CFirApp application class. fir.cpp This is the main application source file that contains the application class CFirApp. fir.rc This is a listing of all of the Microsoft Windows resources that the program uses. It includes the icons, bitmaps, and cursors that are stored in the RES subdirectory. This file can be directly edited in Microsoft Visual C++. fir.clw This file contains information used by ClassWizard to edit existing classes or add new classes. ClassWizard also uses this file to store information needed to create and edit message maps and dialog data maps and to create prototype member functions. res\fir.ico This is an icon file, which is used as the application's icon. This icon is included by the main resource file fir.rc. res\fir.rc2 This file contains resources that are not edited by Microsoft Visual C++. You should place all resources not editable by the resource editor in this file. ///////////////////////////////////////////////////////////////////////////// For the main frame window: MainFrm.h, MainFrm.cpp These files contain the frame class CMainFrame, which is derived from CFrameWnd and controls all SDI frame features. ///////////////////////////////////////////////////////////////////////////// AppWizard creates one document type and one view: firDoc.h, firDoc.cpp - the document These files contain your CFirDoc class. Edit these files to add your special document data and to implement file saving and loading (via CFirDoc::Serialize). firView.h, firView.cpp - the view of the document These files contain your CFirView class. CFirView objects are used to view CFirDoc objects. ///////////////////////////////////////////////////////////////////////////// Other standard files: StdAfx.h, StdAfx.cpp These files are used to build a precompiled header (PCH) file named fir.pch and a precompiled types file named StdAfx.obj. Resource.h This is the standard header file, which defines new resource IDs. Microsoft Visual C++ reads and updates this file. ///////////////////////////////////////////////////////////////////////////// Other notes: AppWizard uses "TODO:" to indicate parts of the source code you should add to or customize. If your application uses MFC in a shared DLL, and your application is in a language other than the operating system's current language, you will need to copy the corresponding localized resources MFC42XXX.DLL from the Microsoft Visual C++ CD-ROM onto the system or system32 directory, and rename it to be MFCLOC.DLL. ("XXX" stands for the language abbreviation. For example, MFC42DEU.DLL contains resources translated to German.) If you don't do this, some of the UI elements of your application will remain in the language of the operating system. /////////////////////////////////////////////////////////////////////////////
五子棋是一种古老的策略游戏,它是一种简单而又极具深度的游戏。基于&alpha;-&beta;剪枝算法五子棋人机对战是一种比较经典的实现方式。在Python中,我们可以使用对抗搜索和&alpha;-&beta;剪枝算法实现五子棋人机对战。 首先,我们需要创建一个五子棋的棋盘表示,可以使用二维数组来表示。接着,我们需要编写一个评估函数来评估当前棋盘局面的好坏。评估函数可以根据当前棋盘的情况来给出一个分数,用来评估当前局面的优劣。 接下来,我们可以使用递归的方式来实现对抗搜索和&alpha;-&beta;剪枝算法。对抗搜索是一种搜索算法,它可以搜索当前局面下的所有可能着法,并根据评估函数来选择最优的着法。而&alpha;-&beta;剪枝算法则可以帮助我们剪枝,减少搜索的时间复杂度,从而提高搜索的效率。 在实现对抗搜索和&alpha;-&beta;剪枝算法的过程中,我们需要考虑一些细节问题,比如搜索的深度、搜索的时间、剪枝的条件等等。同时,我们还需要处理一些特殊情况,比如提前胜利、防守对方的提前胜利等等。 最后,我们可以将人机对战的整个过程进行封装,让玩家可以和计算机进行五子棋的对战。玩家可以选择先手或者后手,然后通过与计算机进行对战来提高自己的水平。 综上所述,基于&alpha;-&beta;剪枝算法五子棋人机对战的实现,包括棋盘表示、评估函数、对抗搜索剪枝算法实现,以及人机对战的封装。这样的实现方式既能提高计算机的对战水平,也能帮助玩家提高自己的棋艺水平。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值