赛码网--比大小(Java实现)(康托展开)

本文介绍了一个涉及排列组合的问题,利用康托展开算法求解特定字符排列在所有可能排列中的位置。通过实例演示了如何实现这一算法,并给出了Java代码示例。
题目描述
									

 

现在有"abcdefghijkl”12个字符,将其所有的排列中按字典序排列,给出任意一种排列,说出这个排列在所有的排列中是第几小的?

输入

 

第一行有一个整数n(0<n<=10000);

随后有n行,每行是一个排列;

 

样例输入

 

3

abcdefghijkl

hgebkflacdji

gfkedhjblcia

 

输出

 

输出一个整数m,占一行,m表示排列是第几位;

 

样例输出

 

1

302715242

260726926

 

解题思路:首先说一下我当时的解题思路吧,这个题目很自然的想到要用到排列的知识来解决。于是我设立一个排序好的原数组,也就是刚开始题目给出的。两次循环,外层循环是遍历样例的每个字符,内层循环则是判断他是第几小(也就是它在剩下的元素中的坐标)。我每次找到元素后,就用‘@’替换原来的元素。找出第几小之后做一个累乘。结果:只能通过一个样例(‘abcdefghijk’)。想了很久,一直想不出来自己哪里错了,就从网上百度了一下。

原来这个题目需要用到康托展开,这么一个数学排列公式。康托展开是一个全排列到一个自然数的双射,常用于构建hash表时的空间压缩。设有n个数(1,2,3,4,…,n),可以有组成不同(n!种)的排列组合,康托展开表示的就是是当前排列组合在n个不同元素的全排列中的名次。

大佬博客:https://blog.youkuaiyun.com/wbin233/article/details/72998375上面讲的比较清楚。另外还发现了另外一名大佬的博客,讲的也不错:https://www.cnblogs.com/wsss/p/5473487.html

代码:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

public class Main {
    public static long fac(int n){
        if(n==0||n==1)
            return 1;
        else
            return n*fac(n-1);
    }
    public static void main(String[] args) throws ParseException {
        Scanner sc = new Scanner(System.in);
        int N = sc.nextInt();
        String line = sc.nextLine();
        for(int i=0;i<N;i++){
            String s = sc.nextLine();
            char tmp[]=s.toCharArray();
            long sum = 1;
            for(int j=0;j<12;j++){
                int count = 0;
                for(int k=j+1;k<12;k++){
                    if (tmp[j]>tmp[k])
                        count++;
                }
                sum+=count*fac(11-j);
            }
            System.out.println(sum);
        }
    }
}

 

<think> 我们已知用户需求:查找康托展开的C++实现方法。根据引用内容,我们已有几个实现示例。需要整合这些信息,给出清晰的解释和代码展示。 注意:引用中提供了两种代码实现(引用[1]和引用[2]),引用[3]和引用[4]是概念解释。我们将以引用[1]的代码为主要参考(因为它更直观),并参考引用[2]进行补充说明。 康托展开公式:$X = a_n \cdot (n-1)! + a_{n-1} \cdot (n-2)! + \cdots + a_1 \cdot 0!$,其中$a_i$表示在未出现的元素中比当前位置元素小的个数。 步骤: 1. 初始化结果result=0。 2. 从左到右遍历排列的每个元素(索引i从0开始): a. 统计当前元素右边比它小的元素个数(即还未使用的元素中比当前元素小的个数)作为$a_i$。 b. 将$a_i$乘以$(n-i-1)!$(剩余位置的阶乘),加到result上。 c. 标记当前元素已被使用(在引用[1]中使用了一个used数组来标记,这样在后续统计时就不会再考虑已经用过的数字)。 3. 返回result+1(因为通常排列的排名从1开始)。 引用[2]的实现方式略有不同:它先计算了每个位置上的逆序数(即该元素右边比它小的元素个数),然后依次乘以阶乘。注意,引用[2]中标记的是每个元素右边比它小的个数(注意:这里是在原排列中从该元素右边开始找比它小的,并不考虑左边,而且没有使用used数组,而是直接遍历右边元素)。但是,这种方法实际上是不正确的?因为康托展开要求的是在当前元素之后未出现的元素中比当前元素小的个数,而引用[2]中统计的是原排列中当前元素右边比当前元素小的个数(由于排列是任意的,右边出现的小数可能已经被使用?)。然而,引用[2]的代码中并没有使用used数组,这是因为它在计算每个位置的逆序数时,是在原排列中从该位置向右找比它小的数,这些数在排列中确实出现在后面,但是它们并没有被标记为已使用,所以可能会重复计算?实际上,引用[2]的方法并不符合康托展开的定义,因为康托展开要求的是在未使用的元素中比当前元素小的个数,而不是原排列中右边所有比它小的个数。 因此,我们以引用[1]的方法为准。 代码实现步骤(引用[1]): - 需要一个阶乘函数factorial(n) - 在cantorExpansion函数中: int n = perm.size(); int result = 0; vector<bool> used(n+1, false); // 因为排列中的数字是从1到n,所以需要n+1个空间,索引0不用,从1到n for (int i = 0; i < n; i++) { // 当前数字是perm[i] int smaller_count = 0; // 遍历比当前数字小的所有数字(从1到perm[i]-1),检查它们是否未被使用 for (int j = 1; j < perm[i]; j++) { if (!used[j]) { smaller_count++; } } result += smaller_count * factorial(n - i - 1); used[perm[i]] = true; // 标记当前数字已使用 } return result + 1; // 排名从1开始 注意:引用[1]的代码中,排列perm中的元素是从1开始的(即不是0开始),例如{3,1,2}。 但是,如果我们的排列是从0开始的呢?需要调整吗?在康托展开中,通常排列的元素是1到n。如果排列中有0,那么我们需要调整,但一般情况下康托展开用于1~n的排列。 如果排列是从0开始的,我们需要将数字加1变成1~n?或者修改代码?这里我们按照引用[1]的实现,假设输入排列的元素是从1开始的。 下面我们给出完整的C++代码示例,并添加注释说明。 另外,引用[4]中给出了一个例子:排列“231”在3的全排列中排名第4。我们可以用这个例子测试代码。 测试排列:{2,3,1}(注意:按照引用[4]的例子,排列是2,3,1,但是我们在输入时,排列中的数字应该是数字本身,即2,3,1,而不是字符。另外,注意引用[4]的例子中,排列是1-indexed的,所以直接输入{2,3,1})。 但是,引用[1]的代码中,排列{3,1,2}的结果是多少?按照3的全排列: 1:123, 2:132, 3:213, 4:231, 5:312, 6:321 排列312是第5个,所以{3,1,2}应该是5?但代码中示例是{3,1,2},输出是多少?我们算一下: 第一个数字3:比3小且未使用的有1,2 -> 个数为2,乘以(3-1-1)! = 2!? 不对,应该是(3-0-1)=2,阶乘是2! = 2,所以2*2=4。 然后标记3已使用。 第二个数字1:比1小的没有,所以0,乘以(3-1-1)! = 1! = 1 -> 0。 第三个数字2:比2小的未使用的只有1(但1已经被用了?)所以0,乘以0!=1 -> 0。 总结果4,然后+1=5。正确。 现在计算排列{2,3,1}: 第一个数字2:比2小的未使用的有1(因为1<2),所以1个,乘以(3-0-1)! = 2! = 2 -> 1*2=2。 标记2已使用。 第二个数字3:比3小的未使用的有1(注意,2已经被使用了,所以只有1),所以1个,乘以(3-1-1)! = 1! = 1 -> 1*1=1。 第三个数字1:没有比1小的,0,乘以0! = 1 -> 0。 总结果2+1=3,再加1=4。正确。 因此,代码正确。 下面我们按照引用[1]的代码格式,写出完整的示例代码,并加上详细注释。 注意:阶乘函数可以预先计算并存储,以避免重复计算,特别是当n较大时。但这里n通常不会太大(因为n!增长很快,n>20就很大了),所以这里直接计算。 代码实现: </think> ### 康托展开的C++实现方法 康托展开是将一个排列映射为它在字典序中的排名(从1开始)。以下是基于引用[1][3][4]的规范实现和详细说明: #### 核心公式 对于一个排列 $P = (p_1, p_2, \dots, p_n)$,其康托展开值为: $$X = 1 + \sum_{i=1}^{n} a_i \cdot (n-i)!$$ 其中 $a_i$ 是 $p_i$ 右侧比 $p_i$ 小的数字个数(统计时需排除已使用的数字)[^3][^4]。 --- #### 完整代码实现(带详细注释) ```cpp #include <iostream> #include <vector> using namespace std; // 计算阶乘函数 int factorial(int n) { if (n <= 1) return 1; int res = 1; for (int i = 2; i <= n; ++i) res *= i; return res; } // 康托展开函数 int cantorExpansion(vector<int>& perm) { int n = perm.size(); int result = 0; vector<bool> used(n + 1, false); // 标记数字是否已使用(索引0不用) for (int i = 0; i < n; ++i) { int smaller = 0; // 统计比perm[i]小且未使用的数字数量 for (int j = 1; j < perm[i]; ++j) { if (!used[j]) smaller++; } // 累加当前位的贡献值:count * (剩余位数阶乘) result += smaller * factorial(n - i - 1); used[perm[i]] = true; // 标记当前数字已使用 } return result + 1; // 排名从1开始 } int main() { // 示例:计算排列{2, 3, 1}的排名(应为4) vector<int> perm = {2, 3, 1}; cout << "康托展开值: " << cantorExpansion(perm) << endl; // 输出4 return 0; } ``` #### 关键点解析 1. **阶乘计算**:`factorial()`函数预先计算阶乘值用于权重分配[^1]。 2. **未使用数字统计**:通过`used`数组动态跟踪已处理的数字(核心逻辑)[^1][^3]。 3. **权重累加**:每个位置的贡献值为 $(未使用小数个数) \times (剩余位置阶乘)$[^4]。 4. **字典序偏移**:最终结果`+1`因排名从1开始(而非0)[^3]。 --- #### 示例验证 对于3位数排列 $\{2,3,1\}$: 1. 首位`2`:比`2`小且未使用的是`1` → $1 \times 2! = 2$ 2. 次位`3`:比`3`小且未使用的是`1` → $1 \times 1! = 1$ 3. 末位`1`:无数更小 → $0 \times 0! = 0$ 4. 总和 $2+1+0=3$,最终排名 $3+1=4$(符合引用[4]的字典序)[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值