前言
这玩意儿真的用得到吗......感觉以后确实是跳着选需要的看比较好,有些可能真碰不到的。
一、康托展开
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 个合并到一起,然后用上述方法操作即可。对于合并操作,可以发现只需要每次让左侧的后缀和右侧的前缀交换位置即可,这个就是一开始讨论的交换左右两部分的问题了。
总结
这仨是真没啥劲啊......