首页 > 试题广场 >

密室闯关

[编程题]密室闯关
  • 热度指数:707 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 256M,其他语言512M
  • 算法知识视频讲解

小团和小美正在密室中解密。他们现在来到了一个新的关卡面前。这一关是一个配合关卡,有n个巨大的齿轮摆成一排,每个齿轮上有两个按钮和按顺时针排成一环的26个大写字母。在齿轮的最上面有一个孔,透过孔可以看到齿轮上方的字母。

小团发现,每次他可以按住一个齿轮的一个按钮,小美就可以顺时针移动这个齿轮,使得孔里看到的字母变为其对应的下一个字母(比如A变为BY变为Z),并且如果小团按下的第一个按钮,则齿轮与上一个齿轮咬合,上一个齿轮的能看见的字母会变为其减1的字母(即B变为AZ变为Y),进行这个操作的时候,不会影响上一个齿轮之前的齿轮。如果小团按下的第二个按钮,则下一个齿轮能看见的字母会变为其减1的字母,同样,这个操作不会影响下一个齿轮之后的齿轮。

如果这个齿轮是第一个齿轮,或者上一个齿轮的字母为A,小团按下第一个按钮后小美将不能移动。同理,如果这个齿轮是最后一个齿轮,或者下一个齿轮的字母为A,小团按下第二个按钮后小美将不能移动。

如果该齿轮上的字母是Z,该齿轮按下按钮后也不能移动。这个齿轮组的某个状态所组成的字符串将会是通关密码。

现在,小团想计算出可以变化出多少种齿轮的组合,他会依据这个数字来计算是否可以暴力计算出密码。请你帮助他。


输入描述:

输入包含多组数据。对于每组数据,将会输入两行。

第一行会输入一个数n,代表齿轮的个数。

接下来一行n个大写字母,代表每个齿轮最开始能看见的字母。



输出描述:

对于每组数据,输出一行一个数字,代表密码可能的组合数,对998244353取模。

示例1

输入

2
BB
3
ABA

输出

3
3

说明

对于样例1,有以下三种最终可能的组合:AC,BB,CA

对于样例2,有以下三种最终可能的组合:BAA,ABA,AAB

对于样例2,一开始的状态如图第一行所示,其中,红色箭头指向的是透过小孔看得见的字母。

小团选择第1个齿轮的第2个按钮按下,小美旋转后,会变为图第二行的状态

小团选择第3个齿轮的第1个按钮按下,小美旋转后,会变为图第三行的状态




备注:

对于40%的数据,

对于100%的数据,,数据不会超过1000组。

这是一道需要DP预处理的数学问题
首先和楼上大佬表述相同,本问题的实质是将 a-z 视为 0-25 的数字,然后只有两种操作:
1、a[i] += 1 并且 a[i - 1] -= 1
2、a[i] -= 1 并且 a[i -  1] += 1
操作的前提是不能让 a[i] 小于 0 或者 大于 25
此处可以发现,无论执行多少次操作,都不会对整个串的权值和造成影响。
比如 ABA = 0+1+0 = 1,能从 ABA 转化的 AAB 和 BAA 的权值都是 1。
也就是说 状态 x 能转移到的 状态集合 s 具有权值和相同的特点。

为了统计 x 能达到多少个状态,也就是统计状态集合 s 的大小,同样也是统计 s 相对应的 sum 可能出现多少次
那么这个问题就变成了用 n 个 0-25 的数字,凑成和为 sum 的方案数
这就是一个经典的 DFS 或者 DP 问题

题目中明确了多组数据,那么显然每次都使用非记忆的 DFS 是不合理的,必须在输入前进行预处理,将所有的 {n, sum} 数对都处理出来,然后每次询问 O1 得到就行了

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

int n;
long long dp[1005][25005];
long long mod = 998244353;
char a[100005];

void init_dp() {
    int maxn = 100, maxm = 25 * maxn;
    dp[0][0] = 1LL;
    for (int i = 1; i <= maxm; i++) {
        dp[0][i] = 0LL;
    }
    for (int i = 1; i <= maxn; i++) {
        dp[i][0] = 1LL;
        for (int j = 1; j <= maxm; j++) {
            for (int k = 0; k < 26; k++) {
                if (j - k >= 0) {
                    dp[i][j] += dp[i - 1][j - k];
                    dp[i][j] %= mod;
                }
            }
        }
    }
}

int main() {
    init_dp();


    while(scanf("%d", &n) != EOF) {
        scanf("%s", a);
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += (a[i] - 'A');
        }
        printf("%lld\n", dp[n][sum] % mod);
    }



    return 0;
}
	


编辑于 2021-10-26 15:05:55 回复(10)
#动态规划
def dp(n,s):
    if s==0:return 1
    if n==1:return 1
    obj = [[0]*(s+1) for _ in range(n+1)]
    for j in range(min(26,s+1)):
        obj[1][j] = 1
    for i in range(2,n+1):
        obj[i][0] = 1
        for j in range(max(s-25*(n-i),0),s+1):
            obj[i][j] = (sum(obj[i-1][j-min(25,j):j+1])) % mo
    return obj[n][s]

"""
#递归
def dp(n,s):
    if s==0:return 1
    elif n==1:
        if s<min(26,s+1):return 1
        else:return 0
    else:
        a = 0
        for k in range(min(26,s+1)):
            a = (a+dp(n-1,s-k)) % mo
        return a
"""

while 1:
    try:
        n = int(input())
        aim = list(input())
        s = 0
        mo = 998244353
        for i in range(n):
            s += (ord(aim[i])-65)
        print(dp(n,s))
    except: break

发表于 2021-09-01 09:41:37 回复(0)

如果该齿轮上的字母是Z,该齿轮按下按钮后也不能移动。

这句话我认为应该去掉

思路:

先转化,抽象出数学语言:
给你一个长度的数组,你每次可以进行如下2个操作,但得保证操作后的数组
(1)
(2)
你可以进行任意次合法操作,问你状态数目

emm这其实演示的是由某一个解得到【total:=\sum{char-'A'+1}的n个和数且和数在[1,26]的Composition的全部解】的过程。

要求的就是total:=\sum{char-'A'+1}分解为n个和数且和数在[1,26]范围内的Composition方案数目

下面的代码利用了公式
n分解为k个和数且和数在[1,r]范围内的Composition方案数目

#include
using namespace std;
typedef long long ll;
#define mod 998244353
string s;
ll n;
ll b[6010][6010];
void GetBinomial(){
    for(int i=0;i<=6005;i++) b[i][0]=1;
    for(int  i=1;i<=6005;i++){
        for(int  j=1;j<=i;j++){
            b[i][j]=(b[i-1][j]+b[i-1][j-1])%mod;
        }
    }
} 
ll Comp_n_summands_EachRange_1R(ll n,ll k, ll r){//n的k个和数且和数在[1,r]的Composition的方案数目 
    ll sum=0;
    if(r<=0) return 0;
    if(k<=0) return 0;
    for(ll j=0;j<=k&&j<=(n-k)/r ;j++){
        if(j&1){
            sum=(sum+mod-b[k][j]*b[n-r*j-1][k-1]%mod)%mod;
        }
        else {
            sum=(sum+b[k][j]*b[n-r*j-1][k-1]%mod)%mod;
        }
    }
    return sum;
}
int main(){
    GetBinomial();
    while(cin>>n>>s){
        ll total=0;
        for(ll i=0;i<n;i++)  total+=(s[i]-'A'+1);
        //cout<<"total"<<total<<endl; 
        cout<<Comp_n_summands_EachRange_1R(total,n,26)<<endl;
    }
    return 0;
}
编辑于 2021-03-08 16:00:16 回复(0)

根据热评第一大佬的思路写了一个java版本。
这题和第二题是相同类型,都是n个数构成目标和sum的方案数,只不过每个题都有一些不同的变化。我不知道美团真实笔试是否是这样类型的,前面做了一套也是,2和4是相同类型的图的遍历问题。
过几天会投简历,去试一下真实的笔试现场

import java.util.*;
import java.lang.*;

public class Main{
    static long mod = 998244353;
    // 题目中并没有给n的范围,这个是试出来的,99也可以
    static int maxn = 100;
    static int maxSum = maxn * 26;
    static long[][] dp = new long[maxn+1][maxSum+1];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        initDP();
        while(sc.hasNextLine()){
            int n = sc.nextInt();
            sc.nextLine();
            String s = sc.nextLine();
            int sum = 0;
            for(int i=0;i<n;++i){
                sum += (s.charAt(i)-'A');
            }
            System.out.println(dp[n][sum]);
        }
    }

    // 求n个0-25的数,组成和为sum的方案数
    // dp[n][sum]
    public static void initDP(){
        // n个数和为0,方案只有1种
        for(int i=0;i<=maxn;++i){
            dp[i][0] = 1;
        }
        // i个数
        for(int i=1;i<=maxn;++i){
            // 目标和为j
            for(int j=1;j<=maxSum;++j){
                // 填入的当前数,0-25,且<=目标和j
                for(int k=0;k<26 && k<=j;++k){
                    dp[i][j] += dp[i-1][j-k];
                    dp[i][j] %= mod;
                }
            }
        }
    }
}
发表于 2022-03-05 15:40:47 回复(2)

将齿轮上的字母映射为权重: A = 0,B = 1,......,Z = 25。通过齿轮的转动,不同齿轮之间的权重可以相互转移,但是所有齿轮的权重和会保持不变,并且每个齿轮的权重只能在 [0, 25]

假设所有齿轮的权重和等于 sum,共有 n 个齿轮,题目就等价于:将 sum 分成有序的 n 份,且每一份都必须在区间 [0, 25] 之间,求总共有多少种方案?

这里采用动态规划分析:

  • 第一步,设 dp[i][j] 表示,将权重 j 分成有序的 i+1 份,总共有几种方案

  • 第二步,第 i 个可以是 0~25,所以 dp[i-1][j] 等于:

  • 第三步,初始化

    • dp[0][j] = (j ≥ 0 && j ≤ 25) ? 1 : 0

    • dp[i][0] = 1

  • 第四步,遍历方向,i 从小到大遍历,j 从小到大

  • 第五步,自己手动演示一下案例,加深理解~

优化:其实不管字符是怎么样的,只要 sum 相等、n 相等,结果一定是相等的。所以我们可以一次性算完所有的 dp,后续输入任何案例从 dp 数组中获取即可,而不是每输入一个案例就 dp 一次。这里可以尝试案例,n 最大是 100 ~

import java.util.*;
import java.io.*;

public class Main {
    private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    private static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));

    private static int maxN = 100;
    private static int maxSum = 2500;
    // dp[i][j] 表示,将权重 j 分成有序的 i+1 份,总共有几种方案
    private static int[][] dp = new int[maxN][maxSum + 1];

    // 初始化 dp
    private static void initDp() {
        // 初始化
        for (int j = 0; j <= maxSum; ++j) {
            dp[0][j] = (j >= 0 && j <= 25) ? 1 : 0;
        }
        for (int i = 0; i < maxN; ++i) {
            dp[i][0] = 1;
        }
        // 遍历
        for (int i = 1; i < maxN; ++i) {
            for (int j = 1; j <= maxSum; ++j) {
                for (int k = 0; k <= 25 && k <= j; ++k) {
                    dp[i][j] += dp[i-1][j-k];
                    dp[i][j] %= 998244353;
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        initDp();
        String line = null;
        while (true) {
            line = reader.readLine();
            if (line == null) {
                break;
            }
            // 获取 n 和 sum
            int n = Integer.parseInt(line);
            line = reader.readLine();
            int sum = 0;
            for (int i = 0; i < n; ++i) {
                sum += (line.charAt(i) - 'A');
            }
            // 从 dp 中获取结果
            writer.write("" + dp[n-1][sum]);
            writer.newLine();
        }
        writer.flush();
    }
}
发表于 2023-07-27 11:15:05 回复(0)
转化+dp公式化简
发表于 2021-03-02 15:44:16 回复(0)