wxnp---板子

发布时间 2023-11-24 18:48:40作者: WW爆米花

求逆序对

归并排序

首先是一个排序作用,然后可以求逆序对
平均时间复杂度:O(nlogn),空间复杂度:O(n)

代码:

int n,ans=0;
int a[N],temp[N];
void merge_pai(int l,int r,int mid){
    int i=l,p=l,j=mid;
    while(i<mid&&j<=r){//左右的边界
        if(a[i]<=a[j])temp[p++]=a[i++];//左边小于右边,加入数组
        else{
            temp[p++]=a[j++];//左边大于右边,左边剩余的数都大于右边的这个数(因为左右两边都是升序排列的)
            ans+=mid-i;//刚好是个数
        }
    }
    while(i<mid)temp[p++]=a[i++];//剩下的加入数组
    while(j<=r)temp[p++]=a[j++];//剩下的加入数组
    p=i=l;
    while(p<=r)a[i++]=temp[p++];//转移到答案数组
}
void merge_sort(int l,int r){//多次二分
    if(l<r){
        int mid=(l+r)/2;//分成两份
        merge_sort(l,mid);//左边
        merge_sort(mid+1,r);//右边
        merge_pai(l,r,mid+1);//此时左右两边已经有序,进行归并,第3个参数是右边的第一个元素下标
    }
}
void solve()
{
    cin>>n;//数组元素个数
    for(int i=1;i<=n;i++)cin>>a[i];//元素
    merge_sort(1,n);//1-n进行归并排序
    cout<<ans;//逆序对个数
}

树状数组求逆序对

#define int long long
ll n;
string s;
map<char,queue<ll>>mp;
ll a[N];
ll ans;
ll c[N];
ll low_bit(ll x){
    return x&-x;
}
void add(ll idx){
    for(ll i=idx;i<=n;i+= low_bit(i))
        c[i]+=1;//标记存在
}
ll sum(ll idx){//求小于等于当前数的的数量
    ll res=0;
    for(ll i=idx;i>0;i-= low_bit(i))
        res+=c[i];
    return res;
}
void solve()
{
    cin>>n>>s;s=" "+s;
    for(int i=1;i<s.size();i++){
        mp[s[i]].push(i);
    }
    reverse(s.begin()+1, s.end());
    for(int i=1;i<s.size();i++){
        a[i]=mp[s[i]].front();
        mp[s[i]].pop();
    }
    for(int i=1;i<=n;i++){
        add(a[i]);//这个数值该在的位置+1标记出现
        ans+=i-sum(a[i]);//已经放置的数量-现在小于等于当前数值的数目==已经放置的数目中大于当前数的数量
    }
    cout<<ans;
}

树状数组最基本操作模版

修改和查询的复杂度都是O(nlogN),空间复杂度是O(n)
lowbit,即取2^k,i&(-i)

基本操作包括初始化,单点修改和区间查询
这里是初始化赋值和区间求和

代码:

int n,m;//元素个数,询问个数
int c[N];//树状数组
int low_bit(int x){
    return x&-x;//实现区间跳转
}
void add(int idx,int v){//单点修改,这里是加
    for(int i=idx;i<=n;i+= low_bit(i))//所有包含这个元素的区间都要加
        c[i]+=v;
}
int sum(int idx){
    int ans=0;
    for(int i=idx;i>0;i-= low_bit(i))//个人理解有点分块的意思,求的是当前下标的前缀和
    ans+=c[i];//这个前缀和由好几个区间求得
    return ans;
}
void solve()
{
    cin>>n>>m;
    for(int i=1,x;i<=n;i++){
        cin>>x;
        add(i,x);
    }
    for(int i=1,l,r;i<=m;i++){
        cin>>l>>r;//区间
        cout<<sum(r)-sum(l-1)<<endl;//相当于前缀和
    }
}

线性筛法欧拉筛

时间复杂度O(n)
筛素数+找出最小质因数
代码:

bool is_prime[N];//是否是质数,0为是,1为不是
int prime[N];//质数数组
int top=1;//质数的下标
int min_p[N];//最小质因数数组
void get_prime(int n){
    for(int i=2;i<=n;i++){
        if(!is_prime[i]){//是质数
            prime[top]=i;//存质数
            min_p[i]=i;//质数的最小质因数是本身
            top++;//下标后移
        }
        for(int j=1;j<top;j++){//最小到达遍历质数数组
            if(i*prime[j]>n)break;
            is_prime[i*prime[j]]=1;//标记质数的倍数即合数
            min_p[i*prime[j]]=prime[j];//质数的倍数的最小质因数是该质数
            if(i%prime[j]==0)break;//若i是之前质数的倍数,说明这个倍数会在后面的循环内被筛去,无需继续循环
        }
    }
}

全排列函数及简易用法

next_permutation(start,end),求下一个排列
和prev_permutation(start,end),求上一个排列
举例:

int num[3]={1,2,3};  
    do  
    {  
        cout<<num[0]<<" "<<num[1]<<" "<<num[2]<<endl;  
    }while(next_permutation(num,num+3));

getline()朴素用法

若前面有其他非字符串的输入,要在输入字符串前多写一个getline(cin,s)
如:

void solve()
{
    int n;cin>>n;
    string s;
    getline(cin,s);
    for(int i=1;i<=n;i++){
        getline(cin,s);
        cout<<s<<endl;
    }
}

两个有用的函数

求最大公因数:__gcd()
求最小公倍数:a*b/__gcd()
求对数:__lg()//__是两个_ _连在一起

手写版:

int gcd(int a, int b) {
    return b > 0 ? gcd(b, a % b) : a;
}

倍增st表

O(nlog(n))预处理,O(1)查询

int Min[50010][20],Max[50010][20];//f[i][j]表示从i开始,区间长度为1<<j的范围内(i到i+(1<<j)-1)的最大值或最小值。当j为0时,区间长度为1,就是i本身
void solve()
{
    int n,m;cin>>n>>m;//n是数组元素个数,m是询问个数
    for(int i=1,x;i<=n;i++){
        cin>>x;
        Min[i][0]=Max[i][0]=x;//长度为1,就是i本身
    }
    for(int i=1;i<=19;i++){//i是区间长度,为log(n)(向下取整)
        for(int j=1;j+(1<<i)-1<=n;j++){//j是左端点,j+(1<<i)-1是右端点<=n
            Min[j][i]=min(Min[j][i-1],Min[j+(1<<(i-1))][i-1]);//2^i=2^(i-1)+2^(i-1)
            Max[j][i]=max(Max[j][i-1],Max[j+(1<<(i-1))][i-1]);//相当于分成两个长度为1<<(i-1)长度的区间
        }
    }
    for(int i=0,l,r;i<m;i++){
        cin>>l>>r;//区间左右端点
        int s=__lg(r-l+1);//所求区间长度
        int ma=max(Max[l][s],Max[r-(1<<s)+1][s]);//会有重合部分但不影响(避免没有全部覆盖)
        int mi=min(Min[l][s],Min[r-(1<<s)+1][s]);//区间内的最小值
        cout<<ma-mi<<endl;
    }
}

把十进制数转为二进制数

//bitset<保留几位>(要转换的十进制数)
cout << "35的2进制: " << bitset<8>(35) << endl;      //<8>:表示保留8位输出

利用倍增求lca(树上最近公共祖先)

int dep[N],fa[N][22];//dep[]是节点深度,fa[][j]j是log(n),离当前节点向根节点跳1<<i个节点所到节点
vector<int>g[N];//树
void dfs(int now,int father){//预处理深度、和祖先
    dep[now]=dep[father]+1;//求深度
    fa[now][0]=father;//记录父节点
    for(int i=1;(1<<i)<=dep[now];i++){
        fa[now][i]=fa[fa[now][i-1]][i-1];//倍增原理
    }
    for(int i=0;i<g[now].size();i++){//遍历子节点
        int v=g[now][i];
        if(v==father)continue;
        dfs(v,now);
    }
}
int lca(int u,int v){
    int dep_u=dep[u],dep_v=dep[v];//得到深度
    if(dep_u!=dep_v){//让u是深度更大的点
        if(dep_v>dep_u){
            swap(u,v);
            swap(dep_u,dep_v);
        }
        int d=dep_u-dep_v;//深度差
        for(int i=0;i<=20;i++){//i的最大值是log(n)
            if((1<<i)&d)u=fa[u][i];//转换成二进制串,i是从低位到高位依次取1,其他取0,如1,10,100,对于d的每一个1,要加上2^i
        }//调整到同一高度
    }
    if(u==v)return u;//可以跳到一起,v就是最近公共祖先
    for(int i=20;i>=0;i--){//从大往小跳
        if(dep[fa[u][i]]<=0)continue;//dep不存在
        if(fa[u][i]==fa[v][i])continue;//跳到一起不行
        else{
            u=fa[u][i];//跳 
            v=fa[v][i];//跳
        }
    }
    return fa[u][0];//此时两者上一个就是所求的最近公共祖先
}
void solve()
{
    int n,m,s;cin>>n>>m>>s;
    for(int i=1,a,b;i<n;i++){
        cin>>a>>b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    dfs(s,0);
    while(m--){
        int u,v;cin>>u>>v;
        int ans=lca(u,v);
        cout<<ans<<endl;
    }
}

差分与树上差分

原数列的前缀和的差分数组还是原数列
原数列的差分数组的前缀和也是原数列
| 原数列 | 9 | 4 | 7 | 5 | 9 |
| ------------- |----|----|----|----|----|
| 前缀和 | 9 | 13 | 20 | 25 | 34 |
| 差分数组 | 9 | -5 | 3 | -2 | 4 |
| 前缀和的差分数组 | 9 | 4 | 7 | 5 | 9 |
| 差分数组的前缀和 | 9 | 4 | 7 | 5 | 9 |

树上差分时需要求lca

点差分:
diff表示差分数组
设将两点u、v之间路径上的所有点权增加x,o=lca(u,v),o的父亲节点为p,操作如下

diff[u]+=x;
diff[v]+=x;
diff[o]-=x;
diff[p]-=x;

以当前节点为根的树的节点的权值和,就是当前节点的权值

边差分:
diff表示差分数组
设将两点u、v之间路径上的所有边权增加x,o=lca(u,v),以每条边两端深度较大的节点存储该边的差分数组,操作如下

diff[u]+=x;
diff[v]+=x;
diff[o]-=2*x;

以每个节点为根的树的节点的权值和,就是当前节点到父亲节点的边的最终权值了

点差分代码模版:

vector<int>g[N];
int n,m,dep[N],fa[N][30],ans,num,power[N];
void dfs(int now,int father){//预处理深度、和祖先
    dep[now]=dep[father]+1;//求深度
    fa[now][0]=father;//记录父节点
    for(int i=1;(1<<i)<=dep[now];i++){
        fa[now][i]=fa[fa[now][i-1]][i-1];//倍增原理
    }
    for(int i=0;i<g[now].size();i++){//遍历子节点
        int v=g[now][i];
        if(v==father)continue;
        dfs(v,now);
    }
}
int lca(int u,int v){
    int dep_u=dep[u],dep_v=dep[v];//得到深度
    if(dep_u!=dep_v){//让u是深度更大的点
        if(dep_v>dep_u){
            swap(u,v);
            swap(dep_u,dep_v);
        }
        int d=dep_u-dep_v;//深度差
        for(int i=0;i<=20;i++){//i的最大值是log(n)
            if((1<<i)&d)u=fa[u][i];//转换成二进制串,i是从低位到高位依次取1,其他取0,如1,10,100,对于d的每一个1,要加上2^i
        }//调整到同一高度
    }
    if(u==v)return u;//可以跳到一起,v就是最近公共祖先
    for(int i=20;i>=0;i--){//从大往小跳
        if(dep[fa[u][i]]<=0)continue;//dep不存在
        if(fa[u][i]==fa[v][i])continue;//跳到一起不行
        else{
            u=fa[u][i];//跳
            v=fa[v][i];//跳
        }
    }
    return fa[u][0];//此时两者上一个就是所求的最近公共祖先
}
void get(int u,int father){//遍历一遍就可以求出以每个节点为根节点的权值和
    for(int i=0;i<g[u].size();i++){
        int e=g[u][i];
        if(e==father)continue;
        get(e,u);
        power[u]+=power[e];//加上子节点的权值和
    }
    ans=max(ans,power[u]);//取最大
}
void solve()
{
    cin>>n>>m;
    for(int i=1,x,y;i<n;i++){
        cin>>x>>y;
        g[x].push_back(y);
        g[y].push_back(x);
    }
    dfs(1,0);
    for(int i=1,x,y;i<=m;i++){
        cin>>x>>y;
        int lc=lca(x,y);//公共节点
        ++power[x];++power[y];--power[lc];--power[fa[lc][0]];//树上差分的四个式子
    }
    get(1,0);
    cout<<ans;
}

数位dp

其实是记忆化搜索,把数字按位处理,复杂度大大降低
来个模板:

ll l,r;
ll a[25],f[25][25];//a是数位数组,f[][]第一维是数位,第二维根据题目定
ll dfs(int pos,bool limit,int sum){//数位,是否受限,据题意可变参数
    if(!pos)return 1;//超过了,这里是满足条件返回1
    if(!limit&&f[pos][sum]!=-1)return f[pos][sum];//不受限且搜过了(记忆的是更普遍的不受限的情况)
    int up=limit?a[pos]:9;//受限就是数位上的值,不受限则可取1-9
    ll res=0;
    for(int i=0;i<=up;i++){//枚举
        if(i==0)res+=dfs(pos-1,limit&&i==up,sum);//这里据题意
        else if(sum!=3)res+=dfs(pos-1,limit&&i==up,sum+1);//这里据题意
    }
    if(!limit)f[pos][sum]=res;//记忆
    return res;
}
ll pre(ll x){
    int len=0;
    while(x>0){//记录每个数位上的值
        a[++len]=x%10;
        x/=10;
    }
    return dfs(len,true,0);//从最高位开始搜,最高位limit是0(有限制,不能任意取,且无前导0,最后一个参数根据题目具体定)
}
void solve(){
    memset(f,-1,sizeof(f));//f[][]数组初始化为-1
    cin>>l>>r;//边界
    ll ans=pre(r)-pre(l-1);
    cout<<ans<<endl;
}

滑动窗口dp

维护一段区间内的最大/小值,手写模拟单调队列实现
代码注释很详尽了

int a[N],dp[N],q[N],tt=-1,hh;//a[]是原数组,q[]是模拟单调队列,里面是元素下标,tt是队尾(拓展的那端),hh是队头(缩减的那端),初始化tt<hh(表示队列没有元素)
void solve(){
    int n;//滑动窗口长度
    for(int i=1;i<=n;i++){
        if(tt>=hh&&i-n+1>q[hh])hh++;//队列有元素,如果队头已经不在窗口中就把队头缩减(窗口长度只有n)
        while(tt>=hh&&dp[q[tt]]>=dp[i])tt--;//队列有元素,现在是单调递增队列,求窗口内最小,则若窗口中从后往前有大于当前数的元素,则它们一定不是最小的,就出队列
        //while(tt>=hh&&dp[q[tt]]<=dp[i])tt--;//同上,现在是单调递减队列,求窗口内最大,则若窗口中从后往前有小于当前数的元素,则它们一定不是最大的,就出队列
        q[++tt]=i;//把当前元素下标放进窗口(队列)
    }
}

叉乘

向量乘积的模可以解释为以a和b为邻边的平行四边形的面积,三角形则再除以2
用行列式来计算的
i j k
ax ay az
bx by bz
cx cy cz

二维平面的话是axby-aybx

可用叉乘求三角形面积判0判断三点共线

if((a[j].first-a[i].first)*(a[k].second-a[i].second)-(a[j].second-a[i].second)*(a[k].first-a[i].first)==0)continue;

杂论

1.不存在格点等边三角形,因为由等边三角形面积公式s=a^2*根号3/4可知等边三角形面积一定是无理数,
如果它是格点形的话意味着矩形内除等边三角形外的其他面积都是有理数,有理数-有理数=有理数,与面积是无理数矛盾

很慢的map之unordered_map

unordered_map里有些数据类型不能放,故自定义

struct hash_pair {//相应的名字也要修改为hash_struct,结构体内部代码不动
    template <class T1, class T2>
    size_t operator()(const pair<T1, T2>& p) const
    {
        auto hash1 = hash<T1>{}(p.first);
        auto hash2 = hash<T2>{}(p.second);
        return hash1 ^ hash2;
    }
};
unordered_map<pair<int,int>,int,hash_pair>mp;//自定义修改第三个参数如hash_struct

欧拉函数

欧拉函数φ(n)是小于或等于n的正整数中与n互质的数的数目
单个欧拉函数计算公式:φ(n)=n(1-1/p1)(1-1/p2)……(1-1/pn)

一边分解质因数一边算,时间复杂度O(n)

ll eula(ll n){
    ll ans=n;
    for(ll i=2;i<=n;i++){
        if(n%i==0){//质因数
            while(n%i==0)n/=i;//质因数的最大幂次
            ans=ans/i*(i-1);//φ(n)=n*(1-1/p1)*(1-1/p2)*……*(1-1/pn),把括号内的通分,先除分母再乘分子
        }
    }
    return ans;
}

性质:合数至少有一个不大于根号n的素因子
所以循环只需循环到根号n即可 时间复杂度 O(根号n),特判当n是质数的情况

ll eula(ll n){
    ll ans=n;
    for(ll i=2;i*i<=n;i++){
        if(n%i==0){//质因数
            while(n%i==0)n/=i;//质因数的最大幂次
            ans=ans/i*(i-1);//φ(n)=n*(1-1/p1)*(1-1/p2)*……*(1-1/pn),把括号内的通分,先除分母再乘分子
        }
    }
    if(n>1)ans=ans/n*(n-1);//特判当n是质数时,不经过上面的for循环,贡献是(n-1)
    return ans;
}

欧拉筛可以快速求[1,n]内所有数的欧拉函数,所以在涉及欧拉函数求和时会使用
利用性质:
如果i%p==0,那么φ(ip)=φ(i)p
如果i%p!=0,那么 φ(ip)=φ(i)(p-1) 其中p为质数

bool is_prime[N];//是否是质数,0为是,1为不是
int prime[N];//质数数组
int top=1;//质数的下标
int ola[N];//i的欧拉函数值
ll sum[N];//求1-n的欧拉函数的和
void get_prime(){
    ola[1]=1;
    for(int i=2;i<N;i++){
        if(!is_prime[i]){//是质数
            prime[top]=i;//存质数
            top++;//下标后移
            ola[i]=i-1;//质数的欧拉函数是质数-1
        }
        for(int j=1;j<top;j++){//最小到达遍历质数数组
            if(i*prime[j]>N)break;
            is_prime[i*prime[j]]=1;//标记质数的倍数即合数
            if(i%prime[j]==0){
                ola[i*prime[j]]=ola[i]*prime[j];//如果i%p==0,那么φ(i*p)=φ(i)*p
                break;//若i是之前质数的倍数,说明这个倍数会在后面的循环内被筛去,无需继续循环
            }
            ola[i* prime[j]]=ola[i]*(prime[j]-1);//如果i%p!=0,那么 φ(i*p)=φ(i)*(p-1)   其中p为质数
        }
    }
}
void solve(){
    get_prime();
    for(int i=1;i<N;i++){
        sum[i]+=sum[i-1]+1ll*ola[i];
    }
}

杨辉三角

n次的二项式系数对应杨辉三角形的n+1行
若题目要求取模,且模数不一定为质数,就不能使用逆元,用杨辉三角

ll a[N][N];
void solve(){
    a[0][0]=1;
    for(int i=1;i<N;i++){
        a[i][0]=1;
        for(int j=1;j<=i;j++){
            a[i][j]=a[i-1][j-1]+a[i-1][j];
        }
    }
    for(int i=0;i<N;i++){
        for(int j=0;j<=i;j++){
            cout<<a[i][j]<<"     ";
        }
        cout<<endl;
    }
}

浅谈逆元

逆元仅在所求数与模数互质时存在,如a在p下的逆元,那么a与p必须是互质的
逆元可以理解为是在同余情况下的倒数
设inv(b)为b mod m 意义下的逆元,那么inv(b)相当于1/b(mod m)
那么a/b(mod m)= a/bbinv(b)(mod m)=ainv(b)(mod m)
b
inv(b)与1(mod m)同余
费马小定理:若a与质数p互质,则a^(p-1)与1(mod p)同余
aa^(p-2)与1(mod p)同余,两边同除以a,那么a^(p-2)即为a在模数p下的逆元
也就是说可以通过快速幂求取其逆元
最终式:a/b(mod p)=a
b^(p-2)(mod p)
乘法逆元有两个性质:
①在模特定数的情况下唯一,不存在多解。
②乘法逆元是完全积性函数,也就是说 inv(a)inv(b)=inv(ab)

再看组合数

ll fac[N],inv[N];//阶乘数组,阶乘的逆元数组
const ll mod=998244353;//是质数的模数
ll power(ll x,ll y){//快速幂
    ll ans=1;
    while(y){
        if(y&1)ans=ans*x%mod;
        x=x*x%mod;
        y>>=1;
    }
    return ans;
}
void init(){
    fac[0]=1;//0的阶乘是1
    inv[0]=power(1,mod-2);//1的逆元
    for(ll i=1;i<N;i++){
        fac[i]=i*fac[i-1]%mod;//递推求阶乘
        inv[i]= power(fac[i],mod-2);//递推求阶乘的逆元
    }
}
ll C(ll x,ll y){
    if(x<y)return 0;//c下方的数比上方的数大,否则不合法
    return fac[x]*inv[y]%mod*inv[x-y]%mod;//把除法变成逆元的乘法
}

快速幂

ll ksm(ll x,ll y,ll p){//用法:ksm(2,n,mod);
    ll res=1;
    while(y>0){
        if(y&1)res=res*x%p;
        x=x*x%p;
        y/=2;
    }
    return res%p;
}

分解质因数

void divide(int n){//被分解数n
    for(int i=2;i<=n/i;i++){
        if(n%i==0){
            int s=0;//记录有几个,即指数
            while(n%i==0){
                s++;//个数++
                n/=i;
            }
            cout<<i<<' '<<s<<endl;
        }
    }
    if(n>1)cout<<n<<' '<<1<<endl;//特判是质数的情况
    cout<<endl;
}