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.卡常小技巧
① 位运算乘除
② 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$ ,如果有负边权就赋无限小。
- 把
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 重大修改