[SMOJ1792]冠军

97 篇文章 0 订阅
15 篇文章 0 订阅
这是一个关于计算不同拳手在擂台赛中胜出的方案数的问题。给定每对拳手之间的胜负关系,需要找出在不同人数(2, 4, 8, 16)的情况下,每个拳手能成为冠军的方案数。通过暴力搜索或状态压缩的动态规划方法可以解决这个问题。" 139591879,6738706,前端开发:闪烁圆点加载动画实现,"['前端实战实例', '前端新手入门', '前端动画', '加载动画特效']
摘要由CSDN通过智能技术生成

题目描述

N 个拳手参加擂台赛,这个人的编号是 0 至 N1。有 N 个位置,编号从 0 至 N1。每个位置分配一个拳手,显然共有 N! 种不同的分配方案。

对于一种具体的分配方案,站在位置 0 的拳手与站在位置 1 的拳手比赛,胜者进入下一轮,败者被淘汰。站在位置 2 的拳手与站在位置 3 的拳手比赛,胜者进入下一轮,败者被淘汰,同样道理,站在位置 4 的拳手与站在位置 5 的拳手比赛,胜者进入下一轮,败者被淘汰。。。最终会产生一个冠军拳手。

如下图所示:

已知 N 一定是 2 的若干次幂,而且不超过 16,也就是说 N 是 {2,4,8,16} 之中的某一个数。

现在的问题是:有多少种不同的分配方案,使得第 i 个选手能最终成为冠军?不妨假设该数值是 ansi

你的任务就是输出: ans0,ans1,,ansN1

输入格式 1792.in

第一行,一个整数 N N=2 或者 4 或者 8 或者 16。

接下来是 N N 列的字符矩阵,第 i 行第 j 列的字符如果是’Y’,表示第 i 个拳手如果跟第 j 个拳手直接比赛的话,第 i 个拳手会赢第 j 个拳手,如果字符是‘N’,表示第 i 个拳手会输给第 j 个拳手。

注意:

  1. i 行第 i 列的字符一定是’N’
  2. 拳手的胜负不一定满足传递性。例如:第 i 个拳手能赢第 j 个拳手,第 j 个拳手能赢第 k 个拳手,但第 i 个拳手可能会输给第 k 个拳手。
  3. 如果第 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。

可以看到 N16 是比较小的,从这里入手,似乎可以进行状态压缩。
最后要求的是每个选手胜出的方案数,但只是“胜出”的话,无法确定是从哪里胜出。两个人里面?四个人里面?……因此还要多记一维。
那么应当用 f[set][i] 表示 i set 这群人当中胜出的方案数。

光有了状态的描述还是不够,转移方程也是一个很关键的东西。
设想一下这样的一个情景。

我们要让 A 在四个人当中胜出的话,根据常识,必然先在上轮中胜出,然后会有另一个人(不妨称为 B)与之对战,被其打败。
也就意味着,如果将 set 拆分成两个元素数量相等的子集 Lset Rset ,在 4 个人的 set 里面 i 胜出的方案,根据乘法原理,应该是在 Lset 里面 A 胜出的方案乘上在 Rset 里面 B 胜出的方案。
但是要注意,按照我们这样的记法,set 是无序的!也就是说 {1, 2} 和 {2, 1} 是两种位置安排方案,但却被视作同一集合。

其实很好办。例如还是在上面的图中,我们即使把 Lset Rset 上下交换,会发现 A 最终胜出依然成立。因此每次转移的时候方案数再乘上 2 就好了。
也就可以把状态转移方程写成

f[set][i]=f[set][i]×f[set′′][j]×2 if i,jsetisetjset′′ and i beats j

其中 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值