数据结构与算法:康托展开、约瑟夫环、完美洗牌

前言

这玩意儿真的用得到吗......感觉以后确实是跳着选需要的看比较好,有些可能真碰不到的。

一、康托展开

1.康托展开------康托展开

康托展开就是计算一个排列在字典序中的排名。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<class T>
constexpr T power(T a, ll b) {
    T res = 1;
    for (; b != 0; b /= 2, a *= a) {
        if (b & 1) {
            res *= a;
        }
    }
    return res;
}

template<int M>
struct ModInt {
public:
    constexpr ModInt() : x(0) {}

    template<typename T>
    constexpr ModInt(T x_) {
        T v = x_ % M;
        if (v < 0) {
            v += M;
        }
        x = v;
    }
 
    constexpr int val() const {
        return x;
    }

    constexpr ModInt &operator++() & {
        x++;
        if (x == M) {
            x = 0;
        }
        return *this;
    }

    constexpr ModInt operator++(int) & {
        ModInt res = *this;
        ++(*this);
        return res;
    }

    constexpr ModInt &operator--() & {
        if (x == 0) {
            x = M - 1;
        } else {
            x--;
        }
        return *this;
    }

    constexpr ModInt operator--(int) & {
        ModInt res = *this;
        --(*this);
        return res;
    }
 
    constexpr ModInt operator-() const {
        ModInt res;
        res.x = (x == 0 ? 0 : M - x);
        return res;
    }
 
    constexpr ModInt inv() const {
        return power(*this, M - 2);
    }
 
    constexpr ModInt &operator*=(const ModInt &rhs) &{
        x = ll(x) * rhs.val() % M;
        return *this;
    }
 
    constexpr ModInt &operator+=(const ModInt &rhs) &{
        x += rhs.val();
        if (x >= M) {
            x -= M;
        }
        return *this;
    }
 
    constexpr ModInt &operator-=(const ModInt &rhs) &{
        x -= rhs.val();
        if (x < 0) {
            x += M;
        }
        return *this;
    }
 
    constexpr ModInt &operator/=(const ModInt &rhs) &{
        return *this *= rhs.inv();
    }
 
    friend constexpr ModInt operator*(ModInt lhs, const ModInt &rhs) {
        lhs *= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator+(ModInt lhs, const ModInt &rhs) {
        lhs += rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator-(ModInt lhs, const ModInt &rhs) {
        lhs -= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator/(ModInt lhs, const ModInt &rhs) {
        lhs /= rhs;
        return lhs;
    }
 
    friend constexpr bool operator==(ModInt lhs, const ModInt &rhs) {
        return lhs.val() == rhs.val();
    }
    
    friend constexpr bool operator<(ModInt lhs, const ModInt &rhs) {
        return lhs.val() < rhs.val();
    }
    
    friend constexpr bool operator>(ModInt lhs, const ModInt &rhs) {
        return lhs.val() > rhs.val();
    }
    
    friend constexpr bool operator<=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() <= rhs.val();
    }
    
    friend constexpr bool operator>=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() >= rhs.val();
    }
    
    friend constexpr bool operator!=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() != rhs.val();
    }
 
    friend constexpr std::istream &operator>>(std::istream &is, ModInt &a) {
        ll i;
        is >> i;
        a = i;
        return is;
    }
 
    friend constexpr std::ostream &operator<<(std::ostream &os, const ModInt &a) {
        return os << a.val();
    }
 
private:
    int x;
};

template<int M, typename T = ModInt<M>>
struct Comb {
    vector<T> fac;
    vector<T> inv;
 
    Comb(int n) {
        fac.assign(n, 1);
        for(int i=1;i<n;i++)
        {
            fac[i]=fac[i-1]*i;
        }
        inv.assign(n, 1);
        inv[n-1]=fac[n-1].inv();
        for(int i=n-2;i>=0;i--)
        {
            inv[i]=inv[i+1]*(i+1);
        }
    }
 
    template<std::signed_integral U>
    T P(U n, U m) {
        if(n<m)
        {
            return 0;
        }
        return fac[n] * inv[n - m];
    }
 
    template<std::signed_integral U>
    T C(U n, U m) {
        if(n<m||m<0)
        {
            return 0;
        }
        return fac[n] * inv[n - m] * inv[m];
    }
};
 
//power函数切记强转成 Z !!!!!
constexpr int M = 998244353;
using Z = ModInt<M>;

constexpr int N = 1e6+5;
Comb<M>comb(N);

template<std::signed_integral U>
Z P(U n, U m) {
    return comb.P(n, m);
}
 
template<std::signed_integral U>
Z C(U n, U m) {
    return comb.C(n, m);
}

template<typename T>
struct BIT{
    vector<T>tree;
    int n;

    BIT(int _n,T v=0){
        tree.resize(_n,v);
        n=_n-1;
    }

    int lowbit(int i){
        return i&-i;
    }

    void add(int i,T v){
        while(i<=n){
            tree[i]+=v;
            i+=lowbit(i);
        }
    }

    T sum(int i){
        T ans=0;
        while(i>0){
            ans+=tree[i];
            i-=lowbit(i);
        }
        return ans;
    }

    T query(int l,int r){
        if(r>n||l>r){
            return 0;
        }
        return sum(r)-sum(l-1);
    }

    //第k小
    int kth(int k){
        if(k<1||k>sum(n)){
            return 0;
        }
        
        int p=0;
        for(int i=1<<20;i;i>>=1){
            if(p+i<=n&&tree[p+i]<k){
                k-=tree[p+i];
                p+=i;
            }
        }
        return p+1;
    }
};

void solve()
{
    int n;
    cin>>n;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }

    BIT<int>tree(n+1);
    for(int i=1;i<=n;i++)
    {
        tree.add(i,1);
    }

    Z ans=0;
    for(int i=1;i<=n;i++)
    {
        ans+=tree.sum(a[i]-1)*comb.fac[n-i];

        tree.add(a[i],-1);
    }
    cout<<ans+1<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

举个例子,对于排列 3,4,2,1,考虑求其在字典序中的排名。首先,对于第一个 3,其前面必然有 1 开头和 2 开头的两组排列,每组排列是后续三个数的全排列,所以个数就是 3 的阶乘,再乘以 2 就是比 3 开头小的排列数。之后,对于第二位的 4,也是类似求法。因为 3 已经出现过了,所以第二位的 4 前面必然有 1,2 两个数,所以方案数就是 2 的阶乘再乘以 2。之后依次类推即可。

那么由于每次需要查小于当前数 x 有几个数没出现过,所以可以考虑用树状数组维护。需要注意的是,这样求出来的是前面有几个数,所以最后当前数的排名还需要再加一。

回顾这个过程不难发现,这个本质上是在求阶乘进制下的一个数对应的十进制数。

2.逆康托展开------火星人plus

重点是逆运用,即给出一个排列,求 m 名后的排列。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Segment_Tree
{
    vector<T>data;
    //自定义

    Segment_Tree(int n){
        data.assign(n<<2,0);
        
    }

    //初始全0不用build
    void build(int l,int r,int i)
    {
        if(l==r){
            //自定义
            data[i]=1;
        }   
        else{
            int m=(l+r)>>1;
            build(l,m,i<<1);
            build(m+1,r,i<<1|1);
            up(i);
        }
        //自定义

    }

    void add(int jobi,T jobv,int l,int r,int i){
        if(l==r){
            data[i]+=jobv;
        }
        else{
            int m=(l+r)>>1;

            if(jobi<=m){
                add(jobi,jobv,l,m,i<<1);
            }
            else{
                add(jobi,jobv,m+1,r,i<<1|1);
            }

            up(i);
        }
    }

    T query(int jobl,int jobr,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            return data[i];
        }
        
        int m=(l+r)>>1;

        //自定义
        T ans=0;
        if(jobl<=m){
            ans+=query(jobl,jobr,l,m,i<<1);
        }
        if(m+1<=jobr){
            ans+=query(jobl,jobr,m+1,r,i<<1|1);
        }

        return ans;
    }

    T kth(int k,int l,int r,int i)
    {
        if(l==r)
        {
            data[i]--;
            return l;
        }

        int m=l+r>>1;

        int ans=0;
        if(data[i<<1]>=k)
        {
            ans=kth(k,l,m,i<<1);    
        }
        else
        {
            ans=kth(k-data[i<<1],m+1,r,i<<1|1);
        }

        up(i);
        
        return ans;
    }

    //自定义
    void up(int i){
        data[i]=data[i<<1]+data[i<<1|1];
    }
};

void solve()
{
    int n;ll m;
    cin>>n>>m;
    vector<ll>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }

    Segment_Tree<int>st(n+1);
    st.build(1,n,1);
    for(int i=1;i<=n;i++)
    {
        int x=a[i];
        if(x==1)
        {
            a[i]=0;
        }
        else
        {
            a[i]=st.query(1,x-1,1,n,1);
        }

        st.add(x,-1,1,n,1);
    }

    a[n]+=m;

    for(int i=n;i>=1;i--)
    {
        a[i-1]+=a[i]/(n-i+1);
        a[i]%=n-i+1;
    }

    st.build(1,n,1);
    for(int i=1;i<=n;i++)
    {
        a[i]=st.kth(a[i]+1,1,n,1);
    }

    for(int i=1;i<=n;i++)
    {
        cout<<a[i]<<" ";
    }
    cout<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

因为当排列很长时,排名是存不下的,此时就需要用到阶乘进制了。一个排列阶乘进制下的每一位,就是其树状数组查出来没出现过的数的个数。

在求出该排列对应的阶乘进制下的数后,考虑从低位到高位依次把 m 加进去,每次进位即可。进位的方法是,若当前位的位权是 i 的阶乘,那么下一位的位权 i+1 的阶乘就是在 i 的阶乘的基础上乘以 i+1。所以在进位的时候,进上去的数值就是 m 除以 i+1 向下取整,保留的数值就是 m 模上 i+1 的结果。直接想比较难理解,转化成十进制就可以发现,每次进位就是除以 10 和模 10 的操作,只不过阶乘进制下每次除的数不同而已。

在还原的时候,每次就需要查有 x 个数没出现的数是什么,那么不难想到就是每次用树状数组查第 k 大即可。如果不用树状数组倍增的话,想实现 O(logn) 就只能线段树二分了。 具体实现时,数组末尾为低位,数组开头是高位,下标计算一下即可。

二、约瑟夫环

1.普通约瑟夫环------约瑟夫环

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

void solve()
{
    int n,k;
    cin>>n>>k;

    int cur=1;
    for(int i=2;i<=n;i++)
    {
        cur=(cur+k-1)%i+1;
    }
    cout<<cur<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

整个过程可以看作每次删除一个数后,从下一个人开始重新编号。那么对于某个特定的人,如果能找到上一轮编号和当前轮编号的关系,就可以从最后往回倒推出第一轮中的哪个人留下了。

打表可以发现,新老编号是一个呈现 "剃刀" 形状的函数,那么对于这种函数,肯定就是往取模上考虑。所以若新编号为 x,老编号为 y,上一轮还剩 n 个人,删除节点的编号为 s,那么就有公式 y=(x+s-1)%n+1。那么对于删除节点的编号,若 k 为每轮报的数,打表找规律就有 s=(k-1)%n+1。所以整理就有 y=(x+k-1)%n+1。

2.加强版

一共 n 个点组成的环,游戏 n-1 轮,每轮给定一个数 a[i]。初始从 1 号点开始,每次从 1 开始报数,哪个点报到 a[i] 就删除哪个点。求最终剩下的点的编号。

可以发现其实就是每个数字不同,所以还是倒着推,每次把 k 替换成给定的数字即可。

三、完美洗牌算法

我说限制空间的算法比限制时间的复杂多了有没有懂的,从 Morris 遍历到这个都是......

1.问题描述

给定数组 a,再给定范围 [l,r],保证范围长度 n 是偶数。因为 n 是偶数,所以将数组分为两部分,分别是 。要求在时间复杂度 O(n),空间复杂度 O(1) 的限制下,将数组该范围调整为

2.过程

这个题的重点是空间复杂度 O(1),即不能把整个数组都抄一遍。

首先,先考虑解决交换左右两部分,不使用额外数组的问题。那么就可以先把左侧逆序,再把右侧逆序,然后整体逆序一次即可。

之后,观察操作后每个位置上的数去往的位置,然后找规律。那么对于左侧,若原始位置为 i,左边界为 l,那么操作后去往的位置就是 i+(i-l+1)。类似地,对于右侧,若右边界为 r,那么操作后去往的位置就是 i-(r-i+1)。

因为暴力刷一遍的话,会有大量的数被覆盖掉。又因为此时有了每个位置的数去往的位置,所以可以考虑每次刷完去往的位置后,再从新位置开始,让被替换掉的数再去替换别的数。那么可以发现,操作区间内是存在若干个环的。又因为不能存每个位置是否被换过了,所以还需要考虑优化。

之后有一个结论,当区间长度为 时,每个环的起点分别是区间内第 个数。那么对于符合这个要求的区间,是容易完成操作的。

考虑推广到任意长度的区间。因为符合条度分别件的长是 2,8,26,80,......,又因为区间长度 n 保证是偶数,所以可以以上述的数为位权,对 n 进行进制分解。举个例子,若 n 是 20,那么其首先可以分成 8 长度的块。此时就需要让左侧的前 4 个和右侧的前 4 个合并到一起,然后用上述方法操作即可。对于合并操作,可以发现只需要每次让左侧的后缀和右侧的前缀交换位置即可,这个就是一开始讨论的交换左右两部分的问题了。

总结

这仨是真没啥劲啊......

END

相关推荐
木子墨5162 小时前
LeetCode 热题 100 精讲 | 并查集篇:最长连续序列 · 岛屿数量 · 省份数量 · 冗余连接 · 等式方程的可满足性
数据结构·c++·算法·leetcode
王老师青少年编程3 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【线性扫描贪心】:均分纸牌
c++·算法·编程·贪心·csp·信奥赛·均分纸牌
EQUINOX13 小时前
2026年码蹄杯 本科院校赛道&青少年挑战赛道提高组初赛(省赛)第一场,个人题解
算法
萝卜小白3 小时前
算法实习Day04-MinerU2.5-pro
人工智能·算法·机器学习
Liangwei Lin3 小时前
洛谷 P3133 [USACO16JAN] Radio Contact G
数据结构·算法
weixin_513449964 小时前
PCA、SVD 、 ICP 、kd-tree算法的简单整理总结
c++·人工智能·学习·算法·机器人
code_pgf4 小时前
Qwen2.5-VL 算法解析
人工智能·深度学习·算法·transformer
烟锁池塘柳04 小时前
一文讲透 C++ / Java 中方法重载(Overload)与方法重写(Override)在调用时机等方面的区别
java·c++·面向对象
Code-keys4 小时前
Android Codec2 Filter 算法模块开发指南
android·算法·音视频·视频编解码