OI 基础模板

updated:  2021.2.3 新增线段树,树状数组。

考试必备

1.快速读入

inline ll read()
{
    int x=0,f=0;
    char ch=getchar();
    while(!isdigit(ch)) f|=(ch=='-'),ch=getchar();
    while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return f?-x:x;
}

2.对拍

3.卡常小技巧

① 位运算乘除

[x=x imes2^n ~可以转化成~ x<<n ]

[x=x÷2^n ~可以转化成~ x>>n ]

② for 卡常

for(register int i(1); i<=n; ++i) {}

③ 短函数前加 inline 卡常

④ 判断奇偶

if(a%2==0)  可以转化成  if((a&1)==0)

⑤ 取模用 &

x=667%4;    可以转化成  x=667&(4-1);
x=667%32;   可以转化成  x=667&(32-1);

⑥ 正负转换用位运算

i=-1;       可以转换成  i=~i+1;   或   i=(i^-1)+1; 

⑦ 取绝对值

k=abs(x);   可以转换成  k=(x^(x>>31))-(x >> 31);  

数据结构

1.ST表

静态查询区间最值。

ll f[100001][20];
ll n,m,a[100001];

void ST_make(){
    for(int i=1;i<=n;++i) f[i][0]=a[i];
    ll t=log(n)/log(2)+1;
    for(int j=1;j<t;++j)
        for(int i=1;i<=n-(1<<j)+1;++i)
            f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}

ll ST_q(ll l,ll r)
{
    ll k=log(r-l+1)/log(2);
    return max(f[l][k],f[r-(1<<k)+1][k]);
}

int main()
{
    n=read();
    m=read();
    for(int i=1;i<=n;++i)
    a[i]=read();
    ST_make();
    for(int i=1;i<=m;++i)
    {
        ll l=read(),r=read();
        printf("%lld
",ST_q(l,r));
    }
    return 0;
}

2.树状数组

支持单点修改,区间查询。

ll lowbit(ll x)
{
    return x & (-x);
}

ll c[500002],n,m;

void add(ll x,ll y) //单点修改
{
    for(; x<=n;x+=lowbit(x)) c[x]+=y;
}

ll sum(ll x) //前缀和
{
    ll ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

ll ask(ll l,ll r) //区间查询
{
    return sum(r)-sum(l-1);
}

int main()
{
    n=read();
    m=read();
    for(int i=1;i<=n;++i) //初始化
    {
        ll x=read();
        add(i,x);
    }
    for(int i=1;i<=m;++i)
    {
        ll opt=read();
        if(opt==1) //单点修改
        {
            ll x=read(),k=read();
            add(x,k);
        }
        else if(opt==2) //区间查询
        {
            ll x,y;
            x=read();
            y=read();
            ll ans=ask(x,y);
            printf("%lld
",ans);
        }
    }
    return 0;
}

线段树

注:本线段树使用链表形式(指针),每个结点都是左闭右开区间。

单点修改,区间查询

作用等同于树状数组,但是线段树时间和空间的开销都非常大。

ll n, m;

struct node
{ //树结点
    ll L, R, sum, k;
    node *lc, *rc;
};

void build(node *now, ll l, ll r) //建树
{
    now->L = l;
    now->R = r;
    now->sum = 0;
    if (l + 1 < r) //不是叶子,继续递归
    {
        now->lc = new node;
        now->rc = new node;
        ll mid = (l + r) / 2;
        build(now->lc, l, mid);
        build(now->rc, mid, r);
    }
    else
        now->lc = now->rc = NULL; //是叶子,停下
}

ll check(node *now, ll l, ll r) //查询区间和
{
    if (l <= now->L && now->R <= r + 1) //勿忘 r+1
        return now->sum;
    else
    {
        ll ans = 0;
        ll mid = (now->L + now->R) / 2;
        if (l < mid)
            ans += check(now->lc, l, r);
        if (r >= mid)
            ans += check(now->rc, l, r); //右边需要取等
        return ans;
    }
}

void change(node *now, ll x, ll k) //单点修改
{
    if (now->L + 1 == now->R)
    {
        now->sum += k;
    }
    else
    {
        ll mid = (now->L + now->R) / 2;
        if (x < mid)
            change(now->lc, x, k);
        if (x >= mid)
            change(now->rc, x, k); //右边需要取等
        now->sum = now->lc->sum + now->rc->sum;
    }
}

int main()
{
    n = read();
    m = read();
    node *root;
    root = new node; //创建根结点
    build(root, 1, n + 1);

    for (int i = 1; i <= n; ++i) //初始化
    {
        ll x = read();
        change(root, i, x);
    }

    for (int i = 1; i <= m; ++i)
    {
        ll opt = read();
        if (opt == 1) //区间修改
        {
            ll x = read(), k = read();
            change(root, x, k);
        }
        else if (opt == 2) //区间查询
        {
            ll l = read(), r = read();
            ll res = check(root, l, r);
            printf("%lld
", res);
        }
    }
    return 0;
}

区间修改,区间查询

需要延迟修改。

对于结点,新加 (k),代表延迟修改量。

新增延迟信息下传函数 update()

对于修改和查询函数新增延迟操作。

ll n, m;

struct node
{                    //树结点
    ll L, R, sum, k; //多出了延迟修改量 k
    node *lc, *rc;
};

void build(node *now, ll l, ll r) //建树

void update(node *now) //更新
{
    now->lc->sum += now->k * (now->lc->R - now->lc->L);
    now->rc->sum += now->k * (now->rc->R - now->rc->L);
    now->lc->k += now->k;
    now->rc->k += now->k;
    now->k = 0;
}

ll check(node *now, ll l, ll r) //查询区间和
{
    if (l <= now->L && now->R <= r + 1) //勿忘 r+1
        return now->sum;
    else
    {
        if (now->k != 0) //多了更新这一步
            update(now);
        ll ans = 0;
        ll mid = (now->L + now->R) / 2;
        if (l < mid)
            ans += check(now->lc, l, r);
        if (r >= mid)
            ans += check(now->rc, l, r); //右边需要取等
        return ans;
    }
}

void change(node *now, ll l, ll r, ll k) //区间修改
{
    if (l <= now->L && now->R <= r + 1)
    {
        now->sum += k * (now->R - now->L);
        now->k += k;
    }
    else
    {
        if (now->k != 0) //同样多了更新这一步
            update(now);

        ll mid = (now->L + now->R) / 2;
        if (l < mid)
            change(now->lc, l, r, k);
        if (r >= mid)
            change(now->rc, l, r, k);
        now->sum = now->lc->sum + now->rc->sum;
    }
}

int main()
{
    n = read();
    m = read();
    node *root;
    root = new node; //创建根结点
    build(root, 1, n + 1);

    for (int i = 1; i <= n; ++i) //初始化
    {
        ll x = read();
        change(root, i, i, x); //区间修改单点
    }

    for (int i = 1; i <= m; ++i)
    {
        ll opt = read();
        if (opt == 1) //区间修改
        {
            ll l = read(), r = read(), k = read();
            change(root, l, r, k);
        }
        else if (opt == 2) //区间查询
        {
            ll l = read(), r = read();
            ll res = check(root, l, r);
            printf("%lld
", res);
        }
    }
    return 0;
}

数学

1.线性筛素数

给定一个整数 (n) ,求出 $[2,n] $ 之间的所有素数。

思路:prime 数组存放已经筛出的素数, (m) 代表素数个数(也就是说遍历时从 (1) 遍历到 (m) 即可),v 数组代表有没有被标记,避免重复筛。

int v[maxn],prime[maxn],n,k,t,m;
void primes(int n)
{
    memset(v,0,sizeof(v));//清空标记数组
    m=0;//质数个数
    for(int i=2;i<=n;i++)
    {
        if(!v[i])//未被标记,i为质数
            v[i]=i,    prime[++m]=i; //记录
        for(int j=1;j<=m;j++)
        {
            if(prime[j]>v[i]||prime[j]>n/i) break;//i有更小的质因子,或者超出n的范围
            v[i*prime[j]]=prime[j];//prime[j]为合数 i*prime[j]的最小质因子
        }
    }
}
int main()
{
    scanf("%d",&n);
    primes(n);
    for(int i=1;i<=m;++i)
        printf("%d
",prime[i]);
}

2.最大公约数 ((gcd))

① 标准

inline int gcd(int a,int b) {
    int r;
    while(b>0) 
    {
        r=a%b;
        a=b;
        b=r;
    }
    return a;
}

② 三目

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

③ 位运算

inline int gcd(int a,int b) //a,b不能为0
{ 
    while(b^=a^=b^=a%=b);
    return a;
}

④ 辗转相除法

inline int gcd(int a,int b) 
{
    if(a%b==0) return b;
        else return (gcd(b,a%b));
}

⑤ 辗转相减法

int gcd(int a,int b)
{
    if(a==b)return a;
    return a>b?gcd(a-b,b):gcd(b-a,a);
}

⑥ 外挂(考试禁止)

#include <algorithm>
inline int gcd(int a,int b) 
{
    return __gcd(a,b); //其实可以在主函数里直接用这个
}

分治

1.快速乘法取余

给定三个整数 (a,n,mod) ,求 (a imes n ~\%~mod) 的值。

inline int mult_mod(int a,int n,int mod)
{
    int ans=0;
    while(n>0)
    {
        if(n&1) ans=(ans+a)%mod;
        a = (a+a)%mod;
        n >>= 1;
    }
    return ans;
}

这个东西好像没有必要的样子,貌似只需要 ((a~\%~mod)×(n~\%~mod)~\%~mod) 即可。

2.快速幂(非递归)

给定三个整数 (a,n,mod) ,求 (a^n~\%~mod) 的值。

ll ksm(ll a,ll n,ll mod)
{
    if(a==1 || n==0 ) 
    {
        if(mod==1) return 0;
        return 1;
    }
    ll m=a,ans=1;
    while(n>0)
    {
        if(n&1)
        {
            ans*=m;    
            ans%=mod;
        }
        m*=m;
        m%=mod;    
        n>>=1;
    }
    return ans;
}

3.最长上升子序列 (LIS)

求一个序列的最长上升子序列个数。
本程序采用边读边处理 + 二分法。

ll f[maxn], ans = 1; //注意答案个数初始化为1

int main()
{
    ll n = read();
    for (int i = 1; i <= n; ++i)
    {
        int x = read();
        if (i == 1)
        {
            f[1] = x;
            continue;
        }
        int l = 1, r = ans, mid;
        while (l <= r)
        {
            mid = (l + r) >> 1;
            if (x <= f[mid])
                r = mid - 1;
            else
                l = mid + 1;
        }
        f[l] = x;
        if (l > ans)
            ++ans;
    }
    printf("%lld
", ans);
    return 0;
}

4. lower_bound

使用前提:数列为有序数列。

①数组中二分查找

//查找范围:[ begin , end ) ,左闭右开区间。 
*lower_bound(begin,end,num);  //返回第一个 >= num 的数的数值
lower_bound(begin,end,num)-begin; // 返回下标

实际操作:
int a[100]={0,1,3,5,7,9,10};
//    下标:0 1 2 3 4 5  6
int main()
{
    int x=lower_bound(a+1,a+6+1,6)-a; //输出下标
    int y=*lower_bound(a+1,a+6+1,6);  //输出值
    printf("%d %d",x,y);
    return 0;
}

输出结果:4 7

②结构体内二分查找

结构体内使用 lower_bound 需要重载,下面我们主要对结构体中的 (a) 进行操作。

struct node //开结构体
{
    int a, id; //定义结构体内的两个变量
    node() {}
    node(int x, int y) : a(x), id(y) {}
    bool operator < (const node t) const //重载
    {
        return a < t.a;
    }
}t[1001];

bool cmp(node x,node y) //快排 cmp 比较函数
{
    if(x.a < y.a) return 1; //结构体内按 a 由小到大排序。
    return 0;
}

int main()
{
    int n=read(); //数列中数的个数
    for(int i=1;i<=n;++i){
        t[i].a=read(); //读入数列
        t[i].id=i;
    }
    sort(t+1,t+n+1,cmp); //按小到大排序

    int x,xiabiao,ans;
    x=read(); //需要查找的数字
    xiabiao=lower_bound(t+1,t+n+1,node(x,0))-t; //这样求下标
    ans=(*lower_bound(t+1,t+n+1,node(x,0))).a;  //这样求值
    printf("%d %d
",xiabiao,ans);
    return 0;
}

输入:
5
20 40 30 10 50
35
输出:
4 40

另:upper_bound 的使用与 lower_bound 的使用类似,只不过是严格大于(>)。

图论

前置:链式前向星存图

int head[maxn], dis[maxn];
bool vis[maxn];
struct node
{
    ll nxt, to, w;
}t[maxn];
void add (const int u,const int v,const int w)
{
    t[++tot].to = v;
    t[tot].w = w;
    t[tot].nxt = head[u];
    head[u] = tot;
}

1.Dijkstra 最短路

求单源 (s) 到任意一点的最短路径。最短路径保存在数组 dis 中。

#include <queue>
priority_queue <pair <ll, ll> > q;
void dijkstra(int s)
{
    memset (dis, 0x3f3f3f3f, sizeof (dis));      //初始边无限大
    memset (vis, 0, sizeof (vis));        //结点初始均为访问 
    dis[s] = 0;    //起点到自己距离为0 
    q.push (make_pair (0, s));    //起点进队 
    while (q.size() != 0)
    {
        x = q.top().second;
        q.pop();    //初始结点入队  
        if (vis[x])  
            continue;    //如果走过,直接跳过 
        vis[x] = 1;    //标记已访问 
        for (ll i = head[x]; i!=-1; i = t[i].nxt)
        {
            ll y = t[i].to, z = t[i].w;
            if (dis[y] > dis[x] + z)
            {
                dis[y] = dis[x] + z;    //更新起点到y最短路 
                q.push (make_pair (-dis[y], y));    //d[y]相反数入队,转小根堆
            }
        }
    }
}
int main()
    for(int i=1;i<=n;++i)    head[i]=-1;
    //后面省略

2.SPFA 最短路&最长路&判断负环

SPFA能处理负边权,可以判断负环。也可以求最长路。

①最短路

#include<queue>
queue<int> q;
void SPFA(int s)
{
    fill(dis+1,dis+1+n,2147483647);//初始边无限大    
    memset(vis,0,sizeof vis);
    dis[s]=0;
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        vis[x]=0;
        for(int i=head[x];i!=-1;i=t[i].nxt)
        {
            int y=t[i].to,z=t[i].w;
            if(dis[y]>dis[x]+z)
            {
                dis[y]=dis[x]+z;
                if(vis[y]==0)
                {
                    q.push(y);
                    vis[y]=1;
                }
            }
        }
    }
}

②最长路

可依据最短路代码进行修改。

1. `dis` 数组赋初值时,如果没有负边权就赋 $-1$ ,如果有负边权就赋无限小。
  1. dis[y]>dis[x]+z 中的 > 改成 <

③判断负环

可在最短路代码基础上进行修改。需新加入一个数组 cnt ,专门记录负环。

补充代码:

ll cnt[maxn];//专门记录负环
void SPFA().....
if(dis[y]>dis[x]+z)
{
    dis[y]=dis[x]+z;
    cnt[y]++;
    if(cnt[y]>=n+1)//出现超过n次表示就有负环 
    {
        ans=1; //ans=1代表有负环。
        return;
    } 
    if(vis[y]==0)
    {
        q.push(y);
        vis[y]=1;
    }
}

3.Floyd 全源最短路

inline void floyd()
{
    for(k=1;k<=n;k++)  
        for(i=1;i<=n;i++)  
            for(j=1;j<=n;j++)  
                if(e[i][j]>e[i][k]+e[k][j])   
                    e[i][j]=e[i][k]+e[k][j];  
}

4.并查集

(n) 代表元素个数,(m) 为操作数。

(opt=1) 时,合并集合 (a,b)(opt=2) 时,如果 (a,b) 在同一集合,输出 Y 否则输出 N

int find(int k)
{
    if(f[k]==k)return k;
    return f[k]=find(f[k]);
}

int main()
{
    n=read();
    m=read();
    for(int i=1;i<=n;++i) f[i]=i; //初始化自己的老大是自己
     
    for(int i=1;i<=m;++i)
    {
        int opt,a,b;
        opt=read();
        a=read();  b=read();
        if(opt==1) 
            f[find(a)]=find(b);
        else
        {
            if(find(a)==find(b))
            printf("Y
");
            else printf("N
");
        }
    }
    return 0;
}

5.Prim 最小生成树

int ans,cnt,now=1;//Prim
void prim()
{
    for(int i=2;i<=n;++i)
        dis[i]=MAXN;
        
    for(int i=head[1];i;i=t[i].nxt)
        dis[t[i].to] = min(dis[t[i].to],t[i].w);
    
    while(++cnt<n)
    {
        int minn=MAXN;
        vis[now]=1;
        for(int i=1;i<=n;++i)
        {
            if(vis[i]) continue;
            if(minn > dis[i])
            {
                minn=dis[i];
                now=i;
            }
        } 
        
        ans+=minn;
        
        for(int i=head[now];i;i=t[i].nxt)
        {
            int y=t[i].to, z=t[i].w;
        if(vis[y]) continue;
            if(dis[y] > z)
            {
                dis[y]=z;
            }
        }
    }
}

6.拓扑排序

ll ans[100],cnt; //拓扑序列及其元素个数
ll deg[100]; //所有点的入度

void topsort()
{
    queue<ll> q;
    for(int i=1; i<=n; ++i)
        if(deg[i]==0) //寻找最开始入度就为0的点
            q.push(i); //入队

    while(!q.empty())
    {
        ll x=q.front(); q.pop();
        ans[++cnt]=x; //把队首的元素放进拓扑序列
        for(int i=head[x]; i; i=t[i].nxt)
        {
            ll y=t[i].to; //寻找邻边
            if(--deg[y]==0) //邻边的入度-1,并且判断减后的入度是否为0
                q.push(y); //如果为0就入队
        }
    }
}

int main()
{
    n=read();m=read(); //点,边
    for(int i=1;i<=m;++i)
    {
        ll x=read(),y=read();
        add(x,y);
        deg[v]++; //入度++
    }

    topsort(); //拓扑排序

    if(cnt<n) //拓扑序列的元素个数小于点数,说明有环
        puts("有环");
    else puts("无环");

    for(int i=1;i<=cnt;++i)
        printf("%lld ",ans[i]); //输出拓扑序列
    
    return 0;
}

常用 STL 初步

1.队列(先进先出)

#include<queue>
queue<int> q; //priority_queue<int> q(从大到小排序);
q.empty();  //判断队列是否为空
q.size();   //返回队列长度
q.push(item);   //对于queue,在队尾压入一个新元素
                //对于priority_queue,在基于优先级的适当位置插入新元素
q.pop();    //弹出队首元素

//queue only:
q.front();  //返回队首元素的值,但不删除该元素
q.back();   //返回队尾元素的值,但不删除该元素
     
//priority_queue only:
q.top();    //返回具有最高优先级的元素值,但不删除该元素

2.栈(先进后出)

#include<set>
stack<int> s;
stack< int, vector<int> > stk;  //覆盖基础容器类型,使用vector实现stk
s.empty();  //判断stack是否为空,为空返回true,否则返回false
s.size();   //返回stack中元素的个数
s.pop();    //删除栈顶元素,但不返回其值
s.top();    //返回栈顶元素的值,但不删除此元素
s.push(item);   //在栈顶压入新元素item

3.set(自动从小到大排序,自动去重)

set<int> s;//multiset<int> s (不去重)
set<int>::const_iterator iter; //迭代器 

s.insert();   //插入
s.erase();    //若参数为元素值,则删除所有该元素值
              //若参数为迭代器,则删除该迭代器指向的值
s.empty();    //判断set是否为空,为空返回 true,否则返回 false
s.count();    //返回某个值元素的个数
s.clear();    //清除所有元素
s.find();     //查找某元素,找到则返回其迭代器,否则返回 s.end()
s.begin();    //返回指向第一个元素的迭代器
--s.end();    //返回指向最后一个元素的迭代器
*s.begin();   //返回指向第一个元素的值
*--s.end();   //返回指向最后一个元素的值
              //区间形式为 [ begin , end ) ,所以 end 要自减
s.size();     //返回集合中元素的个数
*s.lower_bound(k);    //返回第一个大于等于k的元素值
*s.upper_bound(k);    //返回第一个大于k的元素值 (后继)
              //如果没有符合条件的值,则输出 s.size()

/* 遍历 */
for(iter = s.begin() ; iter != s.end() ; ++iter)
{
    cout<<*iter<<" ";//使用迭代器遍历 
}

4.map

#include<map>
map<string,int> m;//string 是 key,int 是 value。
m.size();   //输出元素个数
m.empty();  //如果 map 为空则返回 true
m.clear();  //删除所有元素
......

m["AKIOI"]=10;
cout<<m["AKIOI"];
输出结果:10

· unordered_map

#include<unordered_map>
用法:与 map 差别不大。
优点:因为内部实现了哈希表,因此其查找速度非常的快
缺点:哈希表的建立比较耗费时间
适用处:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用 unordered_map

EdisonBa

2020.11.6 初次编辑

2020.12.1 重大修改

原文地址:https://www.cnblogs.com/EdisonBa/p/OI.html