UVa 10835 Playing with Coins

问题描述

Jack \texttt{Jack} Jack Jill \texttt{Jill} Jill 对抛硬币生成的序列模式感兴趣。给定抛硬币次数 N N N K K K 个禁止的子模式,我们需要计算不包含任何禁止子模式的序列的概率。

输入

  • 每个测试用例包含两个整数 N N N K K K 1 ≤ N ≤ 50 1 \leq N \leq 50 1N50 0 ≤ K ≤ 50 0 \leq K \leq 50 0K50
  • 接下来 K K K 行,每行是一个由字符 HT 组成的子模式
  • 所有子模式长度相同,且长度不超过 10 10 10
  • 输入以 N = K = 0 N = K = 0 N=K=0 结束

输出

  • 对于每个测试用例,输出概率的最简分数形式 a / b a/b a/b ,如果概率为 0 0 0 则输出 0

解题思路

1. 问题转化

这是一个典型的字符串模式匹配问题。我们需要统计长度为 N N NH/T 序列中,不包含任何给定子模式的序列数量。

总序列数为 2 N 2^N 2N ,因此:
概率 = 安全序列数 2 N \text{概率} = \frac{\text{安全序列数}}{2^N} 概率=2N安全序列数

2. 关键观察

  1. 子模式长度固定:所有禁止子模式长度相同,设为 M M M M ≤ 10 M \leq 10 M10
  2. 状态表示:对于模式匹配问题,我们只需要关心序列的最后 M M M 个字符,因为更早的字符不会影响是否匹配长度为 M M M 的子模式
  3. 位掩码表示:可以用二进制位表示序列:
    • H → 1
    • T → 0
    • 长度为 M M M 的子模式可以表示为一个 M M M 位二进制数

3. 算法设计

状态定义

d p [ length ] [ mask ] dp[\texttt{length}][\texttt{mask}] dp[length][mask] 表示:

  • 当前序列长度为 length \texttt{length} length
  • 序列的最后 M M M 个字符(不足 M M M 位时用前导零补齐)的位掩码为 mask \texttt{mask} mask
  • 值为满足以上条件且不包含任何禁止子模式的序列数量
状态转移

从当前状态 ( length , mask ) (\texttt{length}, \texttt{mask}) (length,mask) 可以:

  1. 添加一个 H (二进制 1)
  2. 添加一个 T (二进制 0)

新的掩码计算方式:
newMask = ( ( mask ≪ 1 ) & ( ( 1 ≪ M ) − 1 ) ) ∣ bit \texttt{newMask} = ((\texttt{mask} \ll 1) \& ((1 \ll M) - 1)) \mid \texttt{bit} newMask=((mask1)&((1M)1))bit
其中:

  • << 1 表示左移一位,相当于去掉最旧的字符
  • & ((1 << M) - 1) 确保掩码保持在 M M M
  • | bit 添加新字符( 0 0 0 1 1 1
禁止模式检查

只有当序列长度 ≥ M − 1 \geq M-1 M1 时,才需要检查新的掩码是否在禁止集合中:

  • 如果 newMask \texttt{newMask} newMask 在禁止集合中,则跳过该转移
  • 否则,继续递归
边界条件
  • length = N \texttt{length} = N length=N 时,找到一个安全序列,返回 1 1 1
  • 使用记忆化搜索避免重复计算

4. 特殊情况处理

  1. K = 0 K = 0 K=0 :没有禁止模式,所有 2 N 2^N 2N 个序列都安全,概率为 1 1 1
  2. M > N M > N M>N :禁止模式长度大于序列长度,不可能出现该模式,所有序列都安全,概率为 1 1 1
  3. 否则:使用动态规划计算安全序列数

5. 概率计算

设安全序列数为 safe \texttt{safe} safe ,总序列数为 2 N 2^N 2N ,则:
概率 = safe 2 N \text{概率} = \frac{\texttt{safe}}{2^N} 概率=2Nsafe
需要化简为最简分数,使用 gcd ⁡ \gcd gcd 函数求最大公约数。

算法复杂度分析

  • 状态数 O ( N × 2 M ) O(N \times 2^M) O(N×2M) ,其中 M ≤ 10 M \leq 10 M10
  • 每个状态转移 O ( 1 ) O(1) O(1)
  • 总复杂度 O ( N × 2 M ) O(N \times 2^M) O(N×2M) ,对于 N ≤ 50 N \leq 50 N50 完全可行
  • 空间复杂度 O ( N × 2 M ) O(N \times 2^M) O(N×2M)

代码实现

// Playing with Coins
// UVa ID: 10835
// Verdict: Accepted
// Submission Date: 2025-12-22
// UVa Run Time: 0.020s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

set<int> forbidden;  // 存储禁止模式的位掩码
int N, K, M;         // N:总长度, K:禁止模式数, M:模式长度
long long dp[64][1024];  // dp[i][j]: 长度i,最后M位的掩码为j

// 记忆化搜索
long long dfs(int length, int mask) {
    if (length >= N) return 1LL;  // 找到一个安全序列
    if (~dp[length][mask]) return dp[length][mask];  // 记忆化
    
    long long &r = dp[length][mask];
    r = 0;
    
    // 尝试添加 H(1) 或 T(0)
    for (int i = 0; i <= 1; i++) {
        int nextMask = (((mask << 1) & ((1 << M) - 1))) | i;
        // 只有长度足够时才检查禁止模式
        if (length < M - 1 || !forbidden.count(nextMask))
            r += dfs(length + 1, nextMask);
    }
    return r;
}

int main() {
    int caseNo = 1;
    while (cin >> N >> K, N) {
        cout << "Case " << caseNo++ << ": ";
        forbidden.clear();
        
        // 读入禁止模式并转换为位掩码
        for (int i = 0; i < K; i++) {
            string pattern;
            cin >> pattern;
            M = pattern.length();
            int mask = 0;
            for (auto p : pattern) mask = (mask << 1) | (p == 'H' ? 1 : 0);
            forbidden.insert(mask);
        }
        
        // 特殊情况处理:没有禁止模式或模式长度大于序列长度
        if (K == 0 || M > N) {
            cout << "1/1\n";
            continue;
        }
        
        // 动态规划计算安全序列数
        memset(dp, -1, sizeof dp);
        long long safe = dfs(0, 0);
        
        // 输出结果
        if (safe == 0) cout << "0\n";
        else {
            long long total = 1LL << N;
            long long g = __gcd(safe, total);
            cout << safe / g << '/' << total / g << '\n';
        }
    }
    return 0;
}

样例解析

样例输入

3 1
HH
3 1
HT
3 2
T
H
0 0

样例输出

Case 1: 5/8
Case 2: 1/2
Case 3: 0

解释

Case   1 \texttt{Case 1} Case 1

  • N = 3 N = 3 N=3 ,禁止模式: HH (二进制 11)
  • 包含 HH 的序列: HHH , HHT , THH (3个)
  • 安全序列: 8 − 3 = 5 8 - 3 = 5 83=5
  • 概率: 5 / 8 5/8 5/8

Case   2 \texttt{Case 2} Case 2

  • 禁止模式: HT (二进制 10)
  • 包含 HT 的序列: HHT , HTH , HTT , THT (4个)
  • 安全序列: 4 4 4
  • 概率: 4 / 8 = 1 / 2 4/8 = 1/2 4/8=1/2

Case   3 \texttt{Case 3} Case 3

  • 禁止模式: T (0)和 H (1)
  • 所有序列都至少包含一个 TH
  • 安全序列: 0 0 0
  • 概率: 0 0 0

总结

本题通过位掩码 + 动态规划(记忆化搜索) 的方法,高效地解决了模式匹配计数问题。关键点在于:

  1. 用二进制位表示字符序列,简化操作
  2. 只需维护最后 M M M 个字符的状态
  3. 记忆化搜索避免重复计算
  4. 特殊情况的预处理
提供的参考引用中未包含关于最小化硬币数量问题的相关信息。 最小化硬币数量问题通常是指在给定不同面额的硬币和一个目标金额的情况下,找出使用最少数量的硬币来凑成该目标金额的方案。这是一个经典的动态规划问题,以下是使用Python实现的解决方案: ```python def coinChange(coins, amount): # 创建一个长度为 amount + 1 的数组 dp,初始值为无穷大 dp = [float('inf')] * (amount + 1) # 目标金额为 0 时,所需硬币数量为 0 dp[0] = 0 # 遍历从 1 到 amount 的每个金额 for i in range(1, amount + 1): # 遍历每种硬币面额 for coin in coins: if i - coin >= 0: # 更新 dp[i] 的值,取当前值和使用该硬币后的最小值 dp[i] = min(dp[i], dp[i - coin] + 1) # 如果 dp[amount] 仍然是无穷大,说明无法凑出目标金额,返回 -1 return dp[amount] if dp[amount] != float('inf') else -1 # 示例使用 coins = [1, 2, 5] amount = 11 result = coinChange(coins, amount) print(f"最少需要的硬币数量: {result}") ``` ### 代码解释 1. **初始化 `dp` 数组**:创建一个长度为 `amount + 1` 的数组 `dp`,初始值为无穷大。`dp[0]` 初始化为 0,表示目标金额为 0 时所需硬币数量为 0。 2. **状态转移方程**:对于每个金额 `i`,遍历每种硬币面额 `coin`,如果 `i - coin >= 0`,则更新 `dp[i]` 为 `min(dp[i], dp[i - coin] + 1)`。 3. **返回结果**:如果 `dp[amount]` 仍然是无穷大,说明无法凑出目标金额,返回 -1;否则返回 `dp[amount]`。 ### 复杂度分析 - **时间复杂度**:$O(amount * n)$,其中 `amount` 是目标金额,`n` 是硬币面额的种类数。 - **空间复杂度**:$O(amount)$,主要用于存储 `dp` 数组。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值