全排列、字典序

目录

字典序排列理论基础

1,字符串和整数的字典序同构

2,拓展字典序

3,多个串的排列拼接问题

4,多个串的交叉合并问题

5,字典序贪心问题

力扣 179. 最大数(排列拼接问题)

力扣 321. 拼接最大数(交叉合并问题)

力扣 988. 从叶结点开始的最小字符串

next_permutation

力扣 31. 下一个排列

力扣 46. 全排列

力扣 47. 全排列 II

HDU 1027 Ignatius and the Princess II

根据序列算字典序序数

力扣 1830. 使字符串有序的最少操作次数

根据字典序序数算序列

力扣 60. 第k个排列

力扣 1643. 第 K 条最小指令

全排列数计算

HDU 1261 字串数

 力扣 62. 不同路径

力扣 2400. 恰好移动 k 步到达某一位置的方法数目

力扣 2514. 统计同位异构字符串数目

 力扣 1569. 将子数组重新排序得到同一个二叉搜索树的方案数

力扣 1916. 统计为蚁群构筑房间的不同顺序

力扣 2954. 统计感冒序列的数目

其他全排列问题

力扣 1175. 质数排列


字典序排列理论基础

1,字符串和整数的字典序同构

问题一:把aaa   bbb   abab三个字符串串起来,使得字典序最大(显然是bbbababaaa)

问题二:把111  222  1212三个整数串起来,使得得到的数最大(显然是2221212111)

显然,这2个问题是同构的。

对于其他的字典序排列问题,也是一样的,字符串的字典序和整数的大小顺序都是同构的。

如果是把 22 112 111 121  122  212  221  2212  1222  2122  2221 串起来呢,是不是眼花缭乱?

为此,我们需要定义一个概念:拓展字典序。

2,拓展字典序

对于串a和串b,把ab串起来得到c=f(a,b),把ba串起来得到d=f(b,a),那么c和d的长度相等。

如果c的字典序小于d的字典序,那么我们说a的拓展字典序小于d的拓展字典序

字典序和拓展字典序的关系:

如果2个串并不是互相包含(包括全等),那么字典序和拓展字典序一致。

如果串a包含串b(a比b长),那么字典序一定是b<a,但拓展字典序取决于具体值

如,字典序123<1234,拓展字典序123<1234

字典序123<1230,拓展字典序123>1230

所以,实际上求拓展字典序,直接用定义就是最好的方法。

3,多个串的排列拼接问题

问题:把 22 112 111 121  122  212  221  2212  1222  2122  2221 串起来,使得得到的数最大。

解决这类问题,只需要按照拓展字典序进行排序,然后依次串起来即可。

4,多个串的交叉合并问题

问题:把多个串交叉合并,使得得到的数最大。如9528和5829最大可以得到9 58 5 29 28

解决这类问题,一个正确但可能不太高效的方法,就是贪心算法,每次取出某个串的第一个元素,化成子问题。

选取的方法就是,寻找所有串中拓展字典序最大的那个串,取出它的第一个元素。

再比如 9528 58289,贪心步骤如下:

9528 58289 取出9

528 58289 取出后面的5

528 8289 取出8

528 289 取出5

28 289 因为28928比28289要大,所以取出后面的2

28 89 取出8 9 2 8

最终答案就是958528928

5,字典序贪心问题

力扣 179. 最大数(排列拼接问题)

给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。

注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。

示例 1:

输入nums = [10,2]
输出:"210"

示例 2:

输入nums = [3,30,34,5,9]
输出:"9534330"

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 109
use std::cmp::Ord;
use std::cmp::Ordering;
use std::string::String;
use std::vec::Vec;
pub fn vec_cmp(nums1: &Vec<char>, nums2: &Vec<char>)-> Ordering{
    for i in 0..nums1.len()+nums2.len(){
        let mut x='0';
        if i<nums1.len(){
            x=nums1[i];
        }else{
            x=nums2[i-nums1.len()];
        }
        let mut y='0';
        if i<nums2.len(){
            y=nums2[i];
        }else{
            y=nums1[i-nums2.len()];
        }
        if x>y{
            return Ordering::Greater;
        }
        if x<y{
            return Ordering::Less;
        }
    }
    return Ordering::Equal;
}

#[derive(PartialOrd,Eq,PartialEq)]
struct Node{
    s:Vec<char>
}
impl Ord for Node {
    #[inline]
    fn cmp(&self, other: &Node) -> Ordering {
        return vec_cmp(&other.s,&self.s);
    }
}
impl Solution {
    pub fn largest_number(nums: Vec<i32>) -> String {
        let mut v:Vec<Node>=Vec::new();
        for x in nums{
            v.push(Node{s:Solution::int_to_string(x)});
        }
        v.sort_by(|a,b| a.cmp(b));
        let mut vans = Vec::new();
        let mut flag = true;
        for x in v{
            for c in x.s{
                vans.push(c);
                if c!='0'{
                    flag=false;
                }
            }
        }
        if flag{
            return String::from("0");
        }
        return vans.into_iter().collect();
    }
    fn int_to_string(x:i32)->Vec<char>{
        if x==0{
            return Vec::from(['0']);
        }
        let mut ans = String::new();
        let mut v=Vec::new();
        let mut x = x;
        let mut c='0';
        while x>0 {
            v.insert(0,(c as u8 + (x%10) as u8)as char);
            x/=10;
        }
        return v;
    }
}

力扣 321. 拼接最大数(交叉合并问题)

给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。

求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。

说明: 请尽可能地优化你算法的时间和空间复杂度。

示例 1:

输入:
nums1 = [3, 4, 6, 5]
nums2 = [9, 1, 2, 5, 8, 3]
k = 5
输出:
[9, 8, 6, 5, 3]

示例 2:

输入:
nums1 = [6, 7]
nums2 = [6, 0, 4]
k = 5
输出:
[6, 7, 6, 0, 4]

示例 3:

输入:
nums1 = [3, 9]
nums2 = [8, 9]
k = 3
输出:
[9, 8, 9]

思路:

用了一个很暴力的思路,直接枚举在2个数组中分别取的数量(枚举),根据数量直接把对应的数列取出来(贪心),直接把2个取出来的数列交叉合并(贪心),最后把所有的合并数组进行比较择优(贪心)。

impl Solution {
    pub fn max_number(nums1: Vec<i32>, nums2: Vec<i32>, k: i32) -> Vec<i32> {
        let mut ans =Vec::new();
        for i in 0..=k{
            if nums1.len()<i as usize || nums2.len()<(k-i) as usize{
                continue;
            }
            let mut v1 = Solution::max_number_with_len_k(&nums1,i);
            let mut v2 = Solution::max_number_with_len_k(&nums2,k-i);
            let mut v_merge = Solution::merge(&mut v1,&mut v2);
            if ans.is_empty(){
                ans = v_merge;
            }else if Solution::is_greater_or_eq(&v_merge,&ans){
                ans = v_merge;
            }
        }
        return ans;
    }
    pub fn max_number_with_len_k(nums: &Vec<i32>, k: i32) -> Vec<i32> {
        let mut id = 0;
        let mut ans =Vec::new();
        println!("start k={}", k);
        for i in 0..k{
            let mut max_id = id;

            println!("id= {}", id);
            for j in id..=(nums.len() as i32 - k + i) {
                if nums[max_id as usize] < nums[j as usize]{
                    max_id = j;
                }
            }
            ans.push(nums[max_id as usize]);
            id = max_id + 1;
        }
        return ans;
    }
    pub fn merge(nums1: & mut Vec<i32>, nums2: & mut Vec<i32>)-> Vec<i32> {
        let mut ans=Vec::new();
        while nums1.len() > 0 && nums2.len() > 0{
            if Solution::is_greater_or_eq(nums1,nums2){
                ans.push(nums1[0]);
                nums1.remove(0);
            }else{
                ans.push(nums2[0]);
                nums2.remove(0);
            }
        }
        for i in 0..nums1.len(){
            ans.push(nums1[i]);
        }
        for i in 0..nums2.len(){
            ans.push(nums2[i]);
        }
        return ans;
    }
    pub fn is_greater_or_eq(nums1: &Vec<i32>, nums2: &Vec<i32>)-> bool{
        for i in 0..nums1.len()+nums2.len(){
            let mut x=0;
            if i<nums1.len(){
                x=nums1[i];
            }else{
                x=nums2[i-nums1.len()];
            }
            let mut y=0;
            if i<nums2.len(){
                y=nums2[i];
            }else{
                y=nums1[i-nums2.len()];
            }
            if x>y{
                return true;
            }
            if x<y{
                return false;
            }
        }
        return true;
    }
}

力扣 988. 从叶结点开始的最小字符串

给定一颗根结点为 root 的二叉树,树中的每一个结点都有一个 [0, 25] 范围内的值,分别代表字母 'a' 到 'z'

返回 按字典序最小 的字符串,该字符串从这棵树的一个叶结点开始,到根结点结束

字符串中任何较短的前缀在 字典序上 都是 较小 的:

  • 例如,在字典序上 "ab" 比 "aba" 要小。叶结点是指没有子结点的结点。 

节点的叶节点是没有子节点的节点。

示例 1:

输入:root = [0,1,2,3,4,3,4]
输出:"dba"

示例 2:

输入:root = [25,1,3,1,3,0,2]
输出:"adz"

示例 3:

输入:root = [2,2,1,null,1,0,null,0]
输出:"abc"

提示:

  • 给定树的结点数在 [1, 8500] 范围内
  • 0 <= Node.val <= 25
class Solution {
public:
	void smallestFromLeaf(TreeNode* root, string cur, string &ans)
	{
		if (!root)return;
		cur.insert(0, 1, char(int('a') + root->val));
		if (!root->left && !root->right)
		{
			ans = min(ans, cur);
			return;
		}
		smallestFromLeaf(root->left, cur, ans);
		smallestFromLeaf(root->right, cur, ans);
	}
	string smallestFromLeaf(TreeNode* root) {
		string ans = " ";
		ans[0] = char(int('z') + 1);
		smallestFromLeaf(root, "", ans);
		return ans;
	}
};

next_permutation

力扣 31. 下一个排列

整数数组的一个 排列  就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1] 。

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
  • 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

示例 1:

输入:nums = [1,2,3]
输出:[1,3,2]

示例 2:

输入:nums = [3,2,1]
输出:[1,2,3]

示例 3:

输入:nums = [1,1,5]
输出:[1,5,1]

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 100
class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        next_permutation(nums.begin(),nums.end());
    }
};

力扣 46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同
class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        int s=1;
        for(int i=1;i<=nums.size();i++)s*=i;
        vector<vector<int>>ans;
        while(s--){
            ans.push_back(nums);
            next_permutation(nums.begin(),nums.end());
        }
        return ans;
    }
};

力扣 47. 全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10
class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        int s=1;
        for(int i=1;i<=nums.size();i++)s*=i;
        map<int,int>m;
        for(auto x:nums)m[x]++;
        for(auto mi:m){
            for(int i=1;i<=mi.second;i++)s/=i;
        }
        vector<vector<int>>ans;
        while(s--){
            ans.push_back(nums);
            next_permutation(nums.begin(),nums.end());
        }
        return ans;
    }
};

HDU 1027 Ignatius and the Princess II

题目:
Description

Now our hero finds the door to the BEelzebub feng5166. He opens the door and finds feng5166 is about to kill our pretty Princess. But now the BEelzebub has to beat our hero first. feng5166 says, "I have three question for you, if you can work them out, I will release the Princess, or you will be my dinner, too." Ignatius says confidently, "OK, at last, I will save the Princess." 

"Now I will show you the first problem." feng5166 says, "Given a sequence of number 1 to N, we define that 1,2,3...N-1,N is the smallest sequence among all the sequence which can be composed with number 1 to N(each number can be and should be use only once in this problem). So it's easy to see the second smallest sequence is 1,2,3...N,N-1. Now I will give you two numbers, N and M. You should tell me the Mth smallest sequence which is composed with number 1 to N. It's easy, isn't is? Hahahahaha......" 
Can you help Ignatius to solve this problem? 
Input

The input contains several test cases. Each test case consists of two numbers, N and M(1<=N<=1000, 1<=M<=10000). You may assume that there is always a sequence satisfied the BEelzebub's demand. The input is terminated by the end of file. 
Output

For each test case, you only have to output the sequence satisfied the BEelzebub's demand. When output a sequence, you should print a space between two numbers, but do not output any spaces after the last number. 
Sample Input

6 4
11 8
Sample Output

1 2 3 5 6 4
1 2 3 4 5 6 7 9 8 11 10

解法一:根据阶乘来计算

因为m<=10000<8!,所以当n>8时,前n-8个数一定是1,2,3,4......n-8

代码:

#include<iostream>
using namespace std;

int fac[8] = { 1, 1, 2, 6, 24, 120, 720, 5040 };
int l[9];

void g(int n, int m,int dn)	//n is from 1 to 8
{
	int flag = (m - 1) / fac[n - 1] + 1;
	cout << l[flag]+dn;
	for (int i = flag; i<8; i++)l[i] = l[i + 1];
	if (n>1)
	{
		cout << " ";
		g(n - 1, m - (flag - 1)*fac[n - 1], dn);
	}
	else cout << endl;
}

void f(int n, int m)
{
	if (n > 8)
	{
		for (int i = 1; i <= n - 8; i++)cout << i << " ";
		g(8, m,n-8);
		return;
	}
	g(n, m, 0);
}

int main()
{
	int n, m;
	while (cin >> n >> m)
	{
		for (int i = 0; i < 9; i++)l[i] = i;
		f(n, m);
	}
	return 0;
}

解法二:利用STL

STL中的next_permutation函数可以直接将数组调整到下一顺序的状态。

函数的2个参数,用法和sort里面一样。

比如,对于长为n的数组l,sort(l,l+n)是排序整个数组,next_permutation是给出整个数组的下一顺序的状态。

代码:
 

#include<iostream>
#include<algorithm>
using namespace std;

int main()
{
	int n, m,l[1001];
	while (cin >> n >> m)
	{
		for (int i = 0; i <= n; i++)l[i] = i;
		while (--m)next_permutation(l, l + n+1);
		for (int i = 1; i <= n; i++)cout << l[i] << ((i < n) ? " " : "\n");
	}
	return 0;
}

根据序列算字典序序数

力扣 1830. 使字符串有序的最少操作次数

给你一个字符串 s (下标从 0 开始)。你需要对 s 执行以下操作直到它变为一个有序字符串:

  1. 找到 最大下标 i ,使得 1 <= i < s.length 且 s[i] < s[i - 1] 。
  2. 找到 最大下标 j ,使得 i <= j < s.length 且对于所有在闭区间 [i, j] 之间的 k 都有 s[k] < s[i - 1] 。
  3. 交换下标为 i - 1​​​​ 和 j​​​​ 处的两个字符。
  4. 将下标 i 开始的字符串后缀反转。

请你返回将字符串变成有序的最少操作次数。由于答案可能会很大,请返回它对 109 + 7 取余 的结果。

示例 1:

输入:s = "cba"
输出:5
解释:模拟过程如下所示:
操作 1:i=2,j=2。交换 s[1] 和 s[2] 得到 s="cab" ,然后反转下标从 2 开始的后缀字符串,得到 s="cab" 。
操作 2:i=1,j=2。交换 s[0] 和 s[2] 得到 s="bac" ,然后反转下标从 1 开始的后缀字符串,得到 s="bca" 。
操作 3:i=2,j=2。交换 s[1] 和 s[2] 得到 s="bac" ,然后反转下标从 2 开始的后缀字符串,得到 s="bac" 。
操作 4:i=1,j=1。交换 s[0] 和 s[1] 得到 s="abc" ,然后反转下标从 1 开始的后缀字符串,得到 s="acb" 。
操作 5:i=2,j=2。交换 s[1] 和 s[2] 得到 s="abc" ,然后反转下标从 2 开始的后缀字符串,得到 s="abc" 。

示例 2:

输入:s = "aabaa"
输出:2
解释:模拟过程如下所示:
操作 1:i=3,j=4。交换 s[2] 和 s[4] 得到 s="aaaab" ,然后反转下标从 3 开始的后缀字符串,得到 s="aaaba" 。
操作 2:i=4,j=4。交换 s[3] 和 s[4] 得到 s="aaaab" ,然后反转下标从 4 开始的后缀字符串,得到 s="aaaab" 。

示例 3:

输入:s = "cdbea"
输出:63

示例 4:

输入:s = "leetcodeleetcodeleetcode"
输出:982157772

提示:

  • 1 <= s.length <= 3000
  • s​ 只包含小写英文字母。

思路:

每次操作都是把当前字符串变成前一个字典序的字符串。

所以答案就是要计算给定字符串在所有字典序排列中的序数。

class Solution {
public:
	Solution() :opt(1000000007) {}
	int makeStringSorted(string s) {
		vector<int>v(26);
		for (auto c : s)v[c - 'a']++;
		for (int i = 0; i < s.length() - 1; i++) {
			makeStringSorted(s.length()-i,v, s[i]);
		}
		return ans % p;
	}
	void makeStringSorted(int s, vector<int>&v, char c) {
		for (int i = 0; i < c - 'a'; i++) {
			if (v[i] == 0)continue;
			v[i] -= 1;
			ans = (ans + opt.getNum(s - 1, v)) % p;
			v[i] += 1;
		}
		v[c - 'a'] -= 1;
	}
	int ans = 0;
	int p = 1000000007;
	Permutations opt;
};

根据字典序序数算序列

力扣 60. 第k个排列

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

"123"
"132"
"213"
"231"
"312"
"321"
给定 n 和 k,返回第 k 个排列。

说明:

给定 n 的范围是 [1, 9]。
给定 k 的范围是[1,  n!]。
示例 1:

输入: n = 3, k = 3
输出: "213"
示例 2:

输入: n = 4, k = 9
输出: "2314"

int listt[9] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320};
 
class Solution {
public:
string getPermutation(int n, int k) {
        vector<int>v(10,1);
        char c='0';
        string s="";
        for(int i=1;i<=9;i++)v[i]=i;
        for(int i=n-1;i>=0;i--)
        {
            int fac=listt[i];
            int r=(k+fac-1)/fac;
            s+=c+v[r];
            k-=(r-1)*fac;
            v.erase(v.begin()+r);
        }
        return s;
    }
};

力扣 1643. 第 K 条最小指令

Bob 站在单元格 (0, 0) ,想要前往目的地 destination :(row, column) 。他只能向  或向  走。你可以为 Bob 提供导航 指令 来帮助他到达目的地 destination 。

指令 用字符串表示,其中每个字符:

  • 'H' ,意味着水平向右移动
  • 'V' ,意味着竖直向下移动

能够为 Bob 导航到目的地 destination 的指令可以有多种,例如,如果目的地 destination 是 (2, 3)"HHHVV" 和 "HVHVH" 都是有效 指令 。

然而,Bob 很挑剔。因为他的幸运数字是 k,他想要遵循 按字典序排列后的第 k 条最小指令 的导航前往目的地 destination 。k  的编号 从 1 开始 。

给你一个整数数组 destination 和一个整数 k ,请你返回可以为 Bob 提供前往目的地 destination 导航的 按字典序排列后的第 k 条最小指令 

示例 1:

输入:destination = [2,3], k = 1
输出:"HHHVV"
解释:能前往 (2, 3) 的所有导航指令 按字典序排列后 如下所示:
["HHHVV", "HHVHV", "HHVVH", "HVHHV", "HVHVH", "HVVHH", "VHHHV", "VHHVH", "VHVHH", "VVHHH"].

示例 2:

输入:destination = [2,3], k = 2
输出:"HHVHV"

示例 3:

输入:destination = [2,3], k = 3
输出:"HHVVH"

提示:

  • destination.length == 2
  • 1 <= row, column <= 15
  • 1 <= k <= nCr(row + column, row),其中 nCr(a, b) 表示组合数,即从 a 个物品中选 b 个物品的不同方案数。
class Solution {
public:
	string kthSmallestPath(vector<int>& v, int k) {
		return kthSmallestPath(v[1], v[0], k);
	}
	string kthSmallestPath(int h,int v, int k) {
		if (h) {
			int c = ComNum::getValue(h - 1 + v, v);
			if (k <= c)return "H" + kthSmallestPath(h - 1, v, k);
			return "V"+ kthSmallestPath(h, v-1, k-c);
		}
		if(v)return "V" + kthSmallestPath(0, v - 1, 1);
		return "";
	}
};

全排列数计算

如果是4种球的数量分别是a,b,c,d,那么排成一列的情况数是(a+b+c+d)!/a!/b!/c!/d!,以此类推。

HDU 1261 字串数

题目:

Description

一个A和两个B一共可以组成三种字符串:"ABB","BAB","BBA". 
给定若干字母和它们相应的个数,计算一共可以组成多少个不同的字符串. 
Input

每组测试数据分两行,第一行为n(1<=n<=26),表示不同字母的个数,第二行为n个数A1,A2,...,An(1<=Ai<=12),表示每种字母的个数.测试数据以n=0为结束. 
Output

对于每一组测试数据,输出一个m,表示一共有多少种字符串. 
Sample Input

2
1 2
3
2 2 2
0
Sample Output

3
90

代码:

import java.util.*;
import java.math.BigInteger;
public class Main {

	public static BigInteger f(int k)
	{
		BigInteger s=new BigInteger("1");
		for(int i=1;i<=k;i++)s=s.multiply(BigInteger.valueOf(i));
		return s;
	}
	
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        while(true)
        {
            int n=Integer.parseInt(cin.next());
            if(n==0)break;
            BigInteger s=new BigInteger("1");
            int sum=0;
            while(n-->0)
            {
            	int k=Integer.parseInt(cin.next());
            	s=s.multiply(f(k));
            	sum+=k;
            }
            BigInteger r=f(sum);
            System.out.println(r.divide(s));
        }
        
    }
}

 力扣 62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109
class ComNum
{
。。。。。。
};
class Solution {
public:
	int uniquePaths(int m, int n) {
		return ComNum().Get(m + n - 2, m - 1);
	}
};

力扣 2400. 恰好移动 k 步到达某一位置的方法数目

给你两个  整数 startPos 和 endPos 。最初,你站在 无限 数轴上位置 startPos 处。在一步移动中,你可以向左或者向右移动一个位置。

给你一个正整数 k ,返回从 startPos 出发、恰好 移动 k 步并到达 endPos 的 不同 方法数目。由于答案可能会很大,返回对 109 + 7 取余 的结果。

如果所执行移动的顺序不完全相同,则认为两种方法不同。

注意:数轴包含负整数

示例 1:

输入:startPos = 1, endPos = 2, k = 3
输出:3
解释:存在 3 种从 1 到 2 且恰好移动 3 步的方法:
- 1 -> 2 -> 3 -> 2.
- 1 -> 2 -> 1 -> 2.
- 1 -> 0 -> 1 -> 2.
可以证明不存在其他方法,所以返回 3 。

示例 2:

输入:startPos = 2, endPos = 5, k = 10
输出:0
解释:不存在从 2 到 5 且恰好移动 10 步的方法。

提示:

  • 1 <= startPos, endPos, k <= 1000
class Solution {
public:
	int numberOfWays(int startPos, int endPos, int k) {
		int d = endPos - startPos;
		d = d < 0 ? -d : d;
		if (k < d || k % 2 != d % 2)return 0;
		return Permutations().getPermutationNum(vector<int>{(k+d)/2, (k - d) / 2}, 1000000007);
	}
};

力扣 2514. 统计同位异构字符串数目

给你一个字符串 s ,它包含一个或者多个单词。单词之间用单个空格 ' ' 隔开。

如果字符串 t 中第 i 个单词是 s 中第 i 个单词的一个 排列 ,那么我们称字符串 t 是字符串 s 的同位异构字符串。

  • 比方说,"acb dfe" 是 "abc def" 的同位异构字符串,但是 "def cab" 和 "adc bef" 不是。

请你返回 s 的同位异构字符串的数目,由于答案可能很大,请你将它对 109 + 7 取余 后返回。

示例 1:

输入:s = "too hot"
输出:18
解释:输入字符串的一些同位异构字符串为 "too hot" ,"oot hot" ,"oto toh" ,"too toh" 以及 "too oht" 。

示例 2:

输入:s = "aa"
输出:1
解释:输入字符串只有一个同位异构字符串。

提示:

  • 1 <= s.length <= 105
  • s 只包含小写英文字母和空格 ' ' 。
  • 相邻单词之间由单个空格隔开。
class Solution {
public:
	int countAnagrams(string s) {
		auto v = StringSplit(s);
		long long ans = 1;
		for (auto s : v) {
			vector<int>v(26);
			for (auto c : s)v[c - 'a']++;
			ans = (ans*Permutations().getPermutationNum(v, 1000000007)) % 1000000007;
		}
		return ans;
	}
};

 力扣 1569. 将子数组重新排序得到同一个二叉搜索树的方案数

给你一个数组 nums 表示 1 到 n 的一个排列。我们按照元素在 nums 中的顺序依次插入一个初始为空的二叉搜索树(BST)。请你统计将 nums 重新排序后,统计满足如下条件的方案数:重排后得到的二叉搜索树与 nums 原本数字顺序得到的二叉搜索树相同。

比方说,给你 nums = [2,1,3],我们得到一棵 2 为根,1 为左孩子,3 为右孩子的树。数组 [2,3,1] 也能得到相同的 BST,但 [3,2,1] 会得到一棵不同的 BST 。

请你返回重排 nums 后,与原数组 nums 得到相同二叉搜索树的方案数。

由于答案可能会很大,请将结果对 10^9 + 7 取余数。

示例 1:

输入:nums = [2,1,3]
输出:1
解释:我们将 nums 重排, [2,3,1] 能得到相同的 BST 。没有其他得到相同 BST 的方案了。

示例 2:

输入:nums = [3,4,5,1,2]
输出:5
解释:下面 5 个数组会得到相同的 BST:
[3,1,2,4,5]
[3,1,4,2,5]
[3,1,4,5,2]
[3,4,1,2,5]
[3,4,1,5,2]

示例 3:

输入:nums = [1,2,3]
输出:0
解释:没有别的排列顺序能得到相同的 BST 。

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= nums.length
  • nums 中所有数 互不相同 。

思路:

以第一个元素x为划分,分为大于x的数和小于x的数

化成2个子问题,合并的时候还要乘一个组合数,即在除x之外的所有位置,选择一些位置来放比x大的数

class Solution {
public:
	int numOfWays(vector<int>& nums) {
		return allNumOfWays(nums) - 1;
	}
	long long allNumOfWays(vector<int>& nums) {
		if (nums.empty())return 1;
		vector<int>v1, v2;
		int x = nums[0];
		for (auto y:nums) {
			if (y > x)v1.push_back(y);
			if (y < x)v2.push_back(y);
		}
		long long ans = allNumOfWays(v1)*allNumOfWays(v2) % 1000000007;
		return ans * ComNum::getValue(nums.size() - 1, v1.size(), 1000000007) % 1000000007;
	}
};

力扣 1916. 统计为蚁群构筑房间的不同顺序

你是一只蚂蚁,负责为蚁群构筑 n 间编号从 0 到 n-1 的新房间。给你一个 下标从 0 开始 且长度为 n 的整数数组 prevRoom 作为扩建计划。其中,prevRoom[i] 表示在构筑房间 i 之前,你必须先构筑房间 prevRoom[i] ,并且这两个房间必须 直接 相连。房间 0 已经构筑完成,所以 prevRoom[0] = -1 。扩建计划中还有一条硬性要求,在完成所有房间的构筑之后,从房间 0 可以访问到每个房间。

你一次只能构筑 一个 房间。你可以在 已经构筑好的 房间之间自由穿行,只要这些房间是 相连的 。如果房间 prevRoom[i] 已经构筑完成,那么你就可以构筑房间 i

返回你构筑所有房间的 不同顺序的数目 。由于答案可能很大,请返回对 109 + 7 取余 的结果。

示例 1:

输入:prevRoom = [-1,0,1]
输出:1
解释:仅有一种方案可以完成所有房间的构筑:0 → 1 → 2

示例 2:

输入:prevRoom = [-1,0,0,1,2]
输出:6
解释:
有 6 种不同顺序:
0 → 1 → 3 → 2 → 4
0 → 2 → 4 → 1 → 3
0 → 1 → 2 → 3 → 4
0 → 1 → 2 → 4 → 3
0 → 2 → 1 → 3 → 4
0 → 2 → 1 → 4 → 3

提示:

  • n == prevRoom.length
  • 2 <= n <= 105
  • prevRoom[0] == -1
  • 对于所有的 1 <= i < n ,都有 0 <= prevRoom[i] < n
  • 题目保证所有房间都构筑完成后,从房间 0 可以访问到每个房间

思路:

利用乘法原理,每个节点贡献一个分量,最后乘起来就行了。

假设一个节点A有3个子节点,以每个节点为根的树中分别有a,b,c个节点,那么根据全排列公式,A节点的贡献分量为(a+b+c)!/a!/b!/c!

利用树形DP,可以统计统计以每个节点为根的树中的所有节点数目。

最后,再根据费马小定理求逆元,剩下的就只是乘法运算了。

性能加速:

技巧一,对阶乘进行预处理,避免重复运算。

技巧二,对阶乘的逆元进行预处理,避免重复运算。

技巧三,对公式进行化简,要求的最终答案可以表示成\frac{n_0!}{\prod n_i},n0是所有节点数目,ni是以每个节点(包括根节点)为根的树中的所有节点数目。

技巧四:内联。因为有可能递归深度非常深,我的代码本来是超时的,手动写了个inline就过了。

性能分析:

分析单次性能的话,有技巧三肯定比没有技巧三要好,因为运算量更少,只需要算1次逆元即可。

但是考虑多次调用接口的均摊性能,因为有技巧一和技巧二,所以用原式子和用技巧三的式子是性能是一样的。甚至有可能原式子更好,因为技巧三的式子中的分母,最多会有多少种情况还不是很好分析,有可能比较大就没法用备忘录去减少重复运算。

代码:

class Solution {
public:
	int waysToBuildRooms(vector<int>& prevRoom) {
		
		map<int, vector<int>>m;
		for (int i = 1; i < prevRoom.size(); i++)m[i].push_back(prevRoom[i]), m[prevRoom[i]].push_back(i);
		auto root = EdgesToMultiTree(m);
		ans = 1;
		dp(root);
        ans=GetInverseNum(ans,p);
		return (ans * opt.getPermutationNum(prevRoom.size(), p)) % p;
	}
	inline int dp(MultiTreeNode* root) {
		int s = 1;
		for (auto son : root->son) {
			s += dp(son);
		}
		ans = (ans * s) % p;
		return s;
	}
	int p = 1000000007;
	long long ans;
	Permutations opt;
};

力扣 2954. 统计感冒序列的数目

给你一个整数 n 和一个下标从 0 开始的整数数组 sick ,数组按 升序 排序。

有 n 位小朋友站成一排,按顺序编号为 0 到 n - 1 。数组 sick 包含一开始得了感冒的小朋友的位置。如果位置为 i 的小朋友得了感冒,他会传染给下标为 i - 1 或者 i + 1 的小朋友,前提 是被传染的小朋友存在且还没有得感冒。每一秒中, 至多一位 还没感冒的小朋友会被传染。

经过有限的秒数后,队列中所有小朋友都会感冒。感冒序列 指的是 所有 一开始没有感冒的小朋友最后得感冒的顺序序列。请你返回所有感冒序列的数目。

由于答案可能很大,请你将答案对 109 + 7 取余后返回。

注意,感冒序列  包含一开始就得了感冒的小朋友的下标。

示例 1:

输入:n = 5, sick = [0,4]
输出:4
解释:一开始,下标为 1 ,2 和 3 的小朋友没有感冒。总共有 4 个可能的感冒序列:
- 一开始,下标为 1 和 3 的小朋友可以被传染,因为他们分别挨着有感冒的小朋友 0 和 4 ,令下标为 1 的小朋友先被传染。
然后,下标为 2 的小朋友挨着感冒的小朋友 1 ,下标为 3 的小朋友挨着感冒的小朋友 4 ,两位小朋友都可以被传染,令下标为 2 的小朋友被传染。
最后,下标为 3 的小朋友被传染,因为他挨着感冒的小朋友 2 和 4 ,感冒序列为 [1,2,3] 。
- 一开始,下标为 1 和 3 的小朋友可以被传染,因为他们分别挨着感冒的小朋友 0 和 4 ,令下标为 1 的小朋友先被传染。
然后,下标为 2 的小朋友挨着感冒的小朋友 1 ,下标为 3 的小朋友挨着感冒的小朋友 4 ,两位小朋友都可以被传染,令下标为 3 的小朋友被传染。
最后,下标为 2 的小朋友被传染,因为他挨着感冒的小朋友 1 和 3 ,感冒序列为  [1,3,2] 。
- 感冒序列 [3,1,2] ,被传染的顺序:[0,1,2,3,4] => [0,1,2,3,4] => [0,1,2,3,4] => [0,1,2,3,4] 。
- 感冒序列 [3,2,1] ,被传染的顺序:[0,1,2,3,4] => [0,1,2,3,4] => [0,1,2,3,4] => [0,1,2,3,4] 。

示例 2:

输入:n = 4, sick = [1]
输出:3
解释:一开始,下标为 0 ,2 和 3 的小朋友没有感冒。总共有 3 个可能的感冒序列:
- 感冒序列 [0,2,3] ,被传染的顺序:[0,1,2,3] => [0,1,2,3] => [0,1,2,3] => [0,1,2,3] 。
- 感冒序列 [2,0,3] ,被传染的顺序:[0,1,2,3] => [0,1,2,3] => [0,1,2,3] => [0,1,2,3] 。
- 感冒序列 [2,3,0] ,被传染的顺序:[0,1,2,3] => [0,1,2,3] => [0,1,2,3] => [0,1,2,3] 。

提示:

  • 2 <= n <= 105
  • 1 <= sick.length <= n - 1
  • 0 <= sick[i] <= n - 1
  • sick 按升序排列。
class Solution {
public:
	int numberOfSequence(int n, vector<int>& sick) {
		vector<int>v;
		sick.push_back(n);
		int t = -1, s = 0;
		for (auto x : sick) {
			if (x - t - 1)v.push_back(x - t - 1), s += x - t - 2;
			t = x;			
		}
		if (sick[0])s -= sick[0] - 1;
		if (sick[sick.size() - 1] - sick[sick.size() - 2] - 1)s -= sick[sick.size() - 1] - sick[sick.size() - 2] - 2;
		int ans = opt.getPermutationNum(v, 1000000007);
		while (s--)ans = (ans * 2) % 1000000007;
		return ans;
	}
	Permutations opt;
};

其他全排列问题

力扣 1175. 质数排列

请你帮忙给从 1 到 n 的数设计排列方案,使得所有的「质数」都应该被放在「质数索引」(索引从 1 开始)上;你需要返回可能的方案总数。

让我们一起来回顾一下「质数」:质数一定是大于 1 的,并且不能用两个小于它的正整数的乘积来表示。

由于答案可能会很大,所以请你返回答案 模 mod 10^9 + 7 之后的结果即可。

示例 1:

输入:n = 5
输出:12
解释:举个例子,[1,2,5,4,3] 是一个有效的排列,但 [5,2,3,4,1] 不是,因为在第二种情况里质数 5 被错误地放在索引为 1 的位置上。

示例 2:

输入:n = 100
输出:682289015

提示:

  • 1 <= n <= 100
class Solution {
public:
	int numPrimeArrangements(int n) {
		auto v = GetPrime(n);
		int num = 0;
		for (auto x : v)if (x <= n)num++;
		long long ans = 1;
		for (int i = 1; i <= num; i++)ans = ans * i % 1000000007;
		for (int i = 1; i <= n- num; i++)ans = ans * i % 1000000007;
		return ans;
	}
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值