题意:
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; }