题意:

N名选手淘汰赛,要求指定的选手M夺冠。给定胜负关系矩阵,Aij=1,代表选手 i 能够战胜选手 j 。要求比赛树的高度尽量小,求所有可能的方案总数


分析:

这个树的高度尽量小很坑很坑,要怎么理解!

树高尽量小是前提,然后才是选手M夺冠

意味着,当N给定时,树的高度已经限定了,因为是淘汰赛,所以当N<=2时,树高h=1

2<N<=4时,h=2。5<N<=8时,h=3。8<N<=16时,h=4!

这里的树高尽量小,是为了解释样例:

4 1
0 0 0 1
1 0 0 1
1 1 0 0
0 0 1 0

N=4,h=2

所以,我们无法构造一棵比赛树使得选手1夺冠

但是,如果没有树高这个限制,我们可以构造出:1 - 4 - 3 - 2

意思是:3与2比赛淘汰2,4与3比赛淘汰3,1与4比赛淘汰4,最终1夺冠~但是不符合树高的条件


根据N的大小,明显是二进制的状态压缩动态规划

定义:dp[i][h][S]为最终获胜为选手 i,树高度为h,选手集合为S的方案总数

我们需要解决几个问题:

A:状态转移方程

我们枚举每一个 i 可以获胜的选手 j,那么可以把选手集合分成S1和S2,使得S1 + S2 = S,S1 | S2 = 0

其中 i in S1,j in S2,S1和S2枚举一个,剩下一个计算并检查

那么,就得到了dp的状态转移(实际在写的时候,这里还是有坑,稍后说明)


B:记忆化搜索的初始化问题

边界条件:什么时候不合法,什么时候是边界(看记忆化搜索的return部分很清楚)


细节部分1:

如何枚举S1和S2:

int set_bits = 13;
for( int set1 = set_bits & ( set_bits - 1 ); set1; set1 = set_bits & ( set1 - 1 ) ){  
    int set2 = set1 ^ set_bits;
    printf("%d %d\n",set1,set2);
}  
printf("-------------\n");
set_bits = 13;
for(int i = 1; i < set_bits; i++){
    int j = set_bits - i;
    if (((i & j) == 0)&&((i | j) == set_bits))
        printf("%d %d\n",i,j);
}

给出对比的样例代码,上面是题解的代码,下面是我的枚举代码,虽然结果相同,但是不是一个数量级的枚举

上面的每次操作得到的都是合法的S1和S2,但是下面的枚举还得检查S1和S2的集合关系,所以导致超时


细节部分2:

如何枚举每一个 i 可以获胜的选手 j

for(int l=0;l<mp[i].size();l++){
    int j=mp[i][l];
    //
}
printf("--------\n");
for(int j=0;j<n;j++)
    if (mp[i][j]){
        //
    }

这里靠的是输入输出的数据变形能力了,题目中给的是胜负关系矩阵,如果对于每个i,都枚举任意j,然后查看胜负关系矩阵,那么是下面的for循环写法;但是,如果输入的时候判断一次选手 i 可以战胜哪些选手 j,只保留这些 i 可以赢的,那么就是写法1,毫无疑问,写法1更优


细节部分3:

高度h的计算:

int h = ceil( log( n ) / log( 2 ) );  
printf("--------\n");
int h=0;
int number=1;
while(number<n){
    number*=2;
    h++;
}
printf("--------\n");
int h=0;
int div=n;
while(div){
    h++;
    div/=2;
}

这里只有写法3是错的,因为当n是2的幂次时,h会比正确值大1~~~


解释清楚了细节,贴两份代码:

//ac
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <string.h>
#include <cmath>
using namespace std;

const int maxn=17;
int n,m;
int dp[maxn][6][1<<maxn];
int mp[maxn][maxn];
int cnt[1<<maxn];

int GETdp(int i,int h,int S){
    if (dp[i][h][S]!=-1) return dp[i][h][S];
    if (S==(1<<i)) return dp[i][h][S]=1;
    if ((1<<h) < cnt[S]) return dp[i][h][S]=0;
    dp[i][h][S]=0;
    for(int set1=S&(S-1);set1;set1=S&(set1-1)){
        if (set1&(1<<i)){
            int set2=set1^S;
            for(int j=0;j<n;j++)
            if (mp[i][j] && (set2 & (1<<j)))
                dp[i][h][S] += GETdp(i,h-1,set1) * GETdp(j,h-1,set2);
        }
    }
    return dp[i][h][S];
}

int main(){
	//freopen("input.txt","r",stdin);
    for( int i = 0; i < ( 1 << 16 ) - 1; ++i )
        cnt[i] = cnt[i >> 1] + ( i & 1 );
	scanf("%d%d",&n,&m);
	m--;
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
			scanf("%d",&mp[i][j]);
	memset(dp,-1,sizeof(dp));
	int h = int(ceil( log( n ) / log( 2 ) ));
	GETdp(m,h,(1<<n)-1);
	if (dp[m][h][(1<<n)-1]==-1) dp[m][h][(1<<n)-1]=0;
	printf("%d\n",dp[m][h][(1<<n)-1]);
	return 0;
}

下面这个是超时代码,仅仅是枚举的姿势不一样(导致了很多无用值和无用判断)

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <string.h>
#include <cmath>
#include <vector>
using namespace std;

const int maxn=17;
int n,m;
int dp[maxn][6][1<<maxn];
int cnt[1<<maxn];
vector< int > mp[maxn];

int GETdp(int i,int h,int S){
    if (dp[i][h][S]!=-1) return dp[i][h][S];
    if (S==(1<<i)) return dp[i][h][S]=1;
    if ((1<<h) < cnt[S]) return dp[i][h][S]=0;
    dp[i][h][S]=0;
    for(int l=0;l<mp[i].size();l++){
        int j=mp[i][l];
        if (S&(1<<j))
            for(int k=(1<<i);k<=S-(1<<j);k++){
                if (((k & (S-k)) == 0) && ((k | (S - k)) == S))
                    if ((k & (1<<i)) && ((S-k) & (1<<j))){
                            dp[i][h][S] += GETdp(i,h-1,k) * GETdp(j,h-1,S-k);
                    }
            }
    }
    /*
    for(int set1=S&(S-1);set1;set1=S&(set1-1)){
        if (set1&(1<<i)){
            int set2=set1^S;
            //for(int j=0;j<n;j++)
            for(int l=0;l<mp[i].size();l++){
                int j=mp[i][l];
                if (set2 & (1<<j))
                    dp[i][h][S] += GETdp(i,h-1,set1) * GETdp(j,h-1,set2);
            }
        }
    }*/
    return dp[i][h][S];
}

int main(){
	//freopen("input.txt","r",stdin);
    for( int i = 0; i < ( 1 << 16 ) - 1; ++i )
        cnt[i] = cnt[i >> 1] + ( i & 1 );
	scanf("%d%d",&n,&m);
	m--;
	int h;
	for(int i=0;i<n;i++){
        mp[i].clear();
        for(int j=0;j<n;j++){
            scanf("%d",&h);
            if (h)
                mp[i].push_back(j);
		}
	}
	memset(dp,-1,sizeof(dp));
	h = int(ceil( log( n ) / log( 2 ) ));
	GETdp(m,h,(1<<n)-1);
	if (dp[m][h][(1<<n)-1]==-1) dp[m][h][(1<<n)-1]=0;
	printf("%d\n",dp[m][h][(1<<n)-1]);
	return 0;
}