题目描述
有 N 个拳手参加擂台赛,这个人的编号是 0 至
N−1 。有 N 个位置,编号从 0 至N−1 。每个位置分配一个拳手,显然共有 N! 种不同的分配方案。对于一种具体的分配方案,站在位置 0 的拳手与站在位置 1 的拳手比赛,胜者进入下一轮,败者被淘汰。站在位置 2 的拳手与站在位置 3 的拳手比赛,胜者进入下一轮,败者被淘汰,同样道理,站在位置 4 的拳手与站在位置 5 的拳手比赛,胜者进入下一轮,败者被淘汰。。。最终会产生一个冠军拳手。
如下图所示:
已知 N 一定是 2 的若干次幂,而且不超过 16,也就是说
N 是 {2,4,8,16} 之中的某一个数。现在的问题是:有多少种不同的分配方案,使得第 i 个选手能最终成为冠军?不妨假设该数值是
ansi 。你的任务就是输出: ans0,ans1,…,ansN−1 。
输入格式 1792.in
第一行,一个整数 N 。
N=2 或者 4 或者 8 或者 16。接下来是 N 行
N 列的字符矩阵,第 i 行第j 列的字符如果是’Y’,表示第 i 个拳手如果跟第j 个拳手直接比赛的话,第 i 个拳手会赢第j 个拳手,如果字符是‘N’,表示第 i 个拳手会输给第j 个拳手。注意:
- 第 i 行第
i 列的字符一定是’N’- 拳手的胜负不一定满足传递性。例如:第 i 个拳手能赢第
j 个拳手,第 j 个拳手能赢第k 个拳手,但第 i 个拳手可能会输给第k 个拳手。- 如果第 i 行第
j 列的字符是’Y’,那么第 j 行第i 列的字符一定是’N’,即拳手 i 和拳手j 比赛,有且只有一个胜者。
输出格式 1792.out
共 N 行,第
i 行是 ansi 。
输入样例 1792.in
输入样例一:
2
NN
YN
输入样例二:
4
NYNY
NNYN
YNNY
NYNN
输入样例三:
8
NYNYNYNY
NNYNYNYY
YNNNNNNN
NYYNNYNY
YNYYNYYY
NYYNNNNN
YNYYNYNN
NNYNNYYN
输出样例 1792.out
输出样例一:
0
2
输出样例二:
8
0
16
0
输出样例三:
4096
8960
0
2048
23808
0
1408
0
样例解释
第一样例:不管拳手1站在位置0还是站在位置1,都能战胜拳手0。
题目大意:有 2n 个选手,相邻选手对战,每轮过后剩下一半的人(即每对对战选手的胜者)。已知选手之间的胜负关系,问各有多少种不同位置安排方案使得每个人最终胜出。
做法一:暴搜。
枚举所有的位置安排方案,即可得到答案。
时间复杂度:
O(n!)
。
得分:53。
代码:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
using namespace std;
const int maxn = 20;
int n;
char a[maxn][maxn];
bool vis[maxn];
int player[maxn];
int ans[maxn];
void solve() {
int m = n;
int f[maxn] = {0};
int g[maxn] = {0};
memcpy(f, player, sizeof(int) * n);
while (m > 1) {
for (int i = 0; i < m; i += 2)
g[i >> 1] = (a[f[i]][f[i | 1]] == 'Y' ? f[i] : f[i | 1]);
m >>= 1;
memcpy(f, g, sizeof(int) * m);
}
++ans[f[0]];
}
void dfs(int k) {
if (k == n) { solve(); return; }
for (int i = 0; i < n; i++)
if (!vis[i]) {
vis[i] = true;
player[k] = i;
dfs(k + 1);
vis[i] = false;
}
}
int main(void) {
freopen("1792.in", "r", stdin);
freopen("1792.out", "w", stdout);
scanf("%d\n", &n);
for (int i = 0; i < n; i++) gets(a[i]);
memset(vis, false, sizeof vis);
memset(player, -1, sizeof player);
dfs(0);
for (int i = 0; i < n; i++) printf("%d\n", ans[i]);
return 0;
}
做法二:状压 DP。
可以看到
N≤16
是比较小的,从这里入手,似乎可以进行状态压缩。
最后要求的是每个选手胜出的方案数,但只是“胜出”的话,无法确定是从哪里胜出。两个人里面?四个人里面?……因此还要多记一维。
那么应当用
f[set][i]
表示
i
在
光有了状态的描述还是不够,转移方程也是一个很关键的东西。
设想一下这样的一个情景。
我们要让 A 在四个人当中胜出的话,根据常识,必然先在上轮中胜出,然后会有另一个人(不妨称为 B)与之对战,被其打败。
也就意味着,如果将
set
拆分成两个元素数量相等的子集
Lset
和
Rset
,在 4 个人的
set
里面
i
胜出的方案,根据乘法原理,应该是在
但是要注意,按照我们这样的记法,set 是无序的!也就是说 {1, 2} 和 {2, 1} 是两种位置安排方案,但却被视作同一集合。
其实很好办。例如还是在上面的图中,我们即使把
Lset
和
Rset
上下交换,会发现 A 最终胜出依然成立。因此每次转移的时候方案数再乘上 2 就好了。
也就可以把状态转移方程写成
其中 set′ 和 set′′ 都是 set 的子集,两个子集的元素数量相等且它们的交集为空集。
但是如果对于每个
set
都再去枚举它的子集的话,会很慢,有被卡常的风险,可以在 DP 前预处理出所有可能的子集。
位运算中有一些常用的小技巧可以优化一下常数,这个就要靠自己去慢慢积累一下了。
总结:一开始很快打出了暴力的做法,但后面却没有想到如何进行 DP。往数学的方向上想过一下,又是通过胜负关系之间的确定来做组合数学之类的东西,终究还是搞不出来。
说明一个什么问题?做 DP 题需要一种敏锐的感觉,看到题目的一些条件就要有意识,然后想想状态怎么表示才合理。
有时即使不同的表示都能做,但是实现的难易程度可能也会各有千秋了。所以这东西还得是靠多练多想多总结,找找感觉吧。
参考代码:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int maxn = 18;
typedef long long LL;
int n, upperLim;
bool a[maxn][maxn]; //a[i][j] 表示 i 能否赢 j
vector <int> win[maxn]; //win[i][j] 为 i 能赢的第 j 个人
LL dp[1 << maxn][maxn];
#define lowbit(x) (x & (-x))
int count(int p) { //求出 p 的二进制中 1 的个数
int ret = 0;
while (p) {
p -= lowbit(p);
ret++;
}
return ret;
}
int log2[1 << maxn];
vector <int> subset[maxn]; //subset[i] 保存了 1 的个数为 i 的所有集合
void prework() { //预处理
memset(log2, 0, sizeof log2);
int x = 1;
for (int i = 0; i < n; i++) {
log2[x] = i;
x <<= 1;
}
memset(subset, 0, sizeof subset);
for (int i = 0; i < upperLim; i++)
subset[count(i)].push_back(i);
}
bool vis[1 << maxn];
void dfs(int state, int m) {
if (vis[state]) return ; else vis[state] = true; //记忆化
if (m == 2) { //边界
for (int i = 0; i < n; i++)
if ((1 << i) & state) {
int j = log2[state ^ (1 << i)];
dp[state][a[i][j] ? i : j] = 2LL;
return ;
}
return ;
}
int cnt = subset[m >> 1].size();
for (int i = 0; i < cnt; i++) //先算出所有子集
if ((state & subset[m >> 1][i]) == subset[m >> 1][i]) dfs(subset[m >> 1][i], m >> 1);
for (int i = 0; i < cnt; i++)
if ((state & subset[m >> 1][i]) == subset[m >> 1][i]) {
int Lset = subset[m >> 1][i];
int Rset = state ^ Lset;
for (int A = 0; A < n; A++) //A 在 Lset 中胜出,B 在 Rset 中胜出且 A 赢 B
if ((1 << A) & Lset)
for (int B = 0; B < win[A].size(); B++)
if ((1 << win[A][B]) & Rset) dp[state][A] += (dp[Lset][A] * dp[Rset][win[A][B]]) * 2LL;
}
}
int main(void) {
freopen("1792.in", "r", stdin);
freopen("1792.out", "w", stdout);
scanf("%d\n", &n);
upperLim = 1 << n;
prework();
memset(win, 0, sizeof win);
for (int i = 0; i < n; i++) {
char str[maxn];
gets(str);
for (int j = 0; j < n; j++) {
a[i][j] = (str[j] == 'Y');
if (i ^ j)
if (a[i][j]) win[i].push_back(j);
}
}
memset(vis, false, sizeof vis);
dfs(upperLim - 1, n);
for (int i = 0; i < n; i++) printf("%lld\n", dp[upperLim - 1][i]);
return 0;
}