- 语法
- 暴力算法
- 计算几何
- 数据结构
- 图论
- 字符串
- 杂项
- 数学
语法
c++
这些定义其实是用来打cf的。语言标准:c++11
#include <bits/stdc++.h>
using namespace std;
#define repeat(i,a,b) for(int i=(a),_=(b);i<_;i++)
#define repeat_back(i,a,b) for(int i=(b)-1,_=(a);i>=_;i--)
#define mst(a,x) memset(a,x,sizeof(a))
#define vector basic_string
#define fi first
#define se second
#ifndef qwq
int cansel_sync=(ios::sync_with_stdio(0),cin.tie(0),0);
#endif
//mt19937 rnd(chrono::high_resolution_clock::now().time_since_epoch().count());
const int N=200010; const double err=1e-11; typedef long long ll; const int inf=~0u>>2; const ll INF=~0ull>>2; ll read(){ll x; if(scanf("%lld",&x)==-1)exit(0); return x;} typedef double lf; typedef long double llf; const lf pi=acos(-1.0); lf readf(){lf x; if(scanf("%lf",&x)==-1)exit(0); return x;} template<typename T> T sqr(const T &x){return x*x;} typedef pair<int,int> pii; template<typename A,typename B> ostream &operator<<(ostream &o,const pair<A,B> &x){return o<<'('<<x.fi<<','<<x.se<<')';}
const int mod=(1?1000000007:998244353); ll mul(ll a,ll b,ll m=mod){return a*b%m;} ll qpow(ll a,ll b,ll m=mod){ll ans=1; for(;b;a=mul(a,a,m),b>>=1)if(b&1)ans=mul(ans,a,m); return ans;} ll getinv(ll v,ll m=mod){return qpow(v,m-2,m);}
//#define int ll
signed main(){
return 0;
}
放在本地的内容(比如可以放进 bits/stdc++.h
)
#define qwq [&]{cerr<<"qwq"<<endl;}()
#define orz(x) [&]{cerr<<#x": "<<x<<endl;}()
#define orzarr(a,n) [&]{cerr<<#a": "; repeat(__,0,n)cerr<<(a)[__]<<" "; cerr<<endl;}()
#define orzeach(a) [&]{cerr<<#a": "; for(auto __:a)cerr<<__<<" "; cerr<<endl;}()
如果没有万能头
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctime>
#include<cctype>
#include<iostream>
#include<algorithm>
//#include<chrono>
#include<vector>
#include<list>
#include<queue>
#include<string>
#include<set>
#include<map>
//#include<unordered_set>
//#include<unordered_map>
删减版(看得清楚点qwq)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll; const int inf=~0u>>2; const ll INF=~0ull>>2;
typedef double lf; const lf err=1e-11;
typedef pair<int,int> pii;
#define repeat(i,a,b) for(int i=(a),_=(b);i<_;i++)
#define repeat_back(i,a,b) for(int i=(b)-1,_=(a);i>=_;i--)
#define mst(a,x) memset(a,x,sizeof(a))
#define qwq (cerr<<"qwq"<<endl)
#define orz(x) (cerr<<#x": "<<x<<endl)
#define orzarr(a,n) {cerr<<#a": "; repeat(__,0,n)cerr<<(a)[__]<<" "; cerr<<endl;}
#define orzeach(a) {cerr<<#a": "; for(auto __:a)cerr<<__<<" "; cerr<<endl;}
#define fi first
#define se second
inline ll read(){ll x; if(scanf("%lld",&x)==-1)exit(0); return x;}
inline lf readf(){lf x; if(scanf("%lf",&x)==-1)exit(0); return x;}
mt19937 rnd(time(0));
const int N=100010;
const int mod=(1?1000000007:998244353);
//#define int ll
signed main(){
ios_base::sync_with_stdio(0); cin.tie(0);
return 0;
}
其他定义
#define lll __int128
#define inline __inline __attribute__((always_inline))
平板电视红黑树
#include <ext/pb_ds/tree_policy.hpp>
#include <ext/pb_ds/assoc_container.hpp>
using namespace __gnu_pbds;
tree<pii,null_type,less<pii>,rb_tree_tag,tree_order_statistics_node_update> t; //红黑树
t.insert({x,i+1}); //----------------- 插入x,用独特的正整数i+1标注(因为erase太辣鸡)
t.erase(t.lower_bound({x,0})); //----- 删除x(删除单个元素)
t.order_of_key({x,0})+1; //----------- x的排名(小于x的元素个数+1)
t.find_by_order(x-1)->first; //------- 排名为x的元素(第x小的数)
prev(t.lower_bound({x,0}))->first; //- x的前驱(小于x且最大)
t.lower_bound({x+1,0})->first; //----- x的后继(大于x且最小)
t.join(t2); //------------------------ 将t2并入t,t2清空,前提是取值范围不相交(所以没卵用)
t.split(v,t2); //--------------------- 小于等于v的元素属于t,其余的属于t2
平板电视优先队列
pairing_heap_tag
配对堆,应该是可并堆里最快的thin_heap_tag
斐波那契堆std::priority_queue
不合并就很快
#include<ext/pb_ds/priority_queue.hpp>
using namespace __gnu_pbds;
__gnu_pbds::priority_queue<int,less<int>,pairing_heap_tag> h; //大根堆
h.push(x); h.top(); h.pop();
h.join(h2); //将h2并入h,h2清空
rope
- (实现方式是块状链表还是平衡树存疑)
#include <ext/rope>
using namespace __gnu_cxx;
rope<int> r; //块状链表
r.push_back(n);
r.insert(pos,n); //插入一个元素
r.erase(pos,len); //区间删除
r.copy(pos,len,x); //区间赋值
r.substr(pos,len); //这是啥
r[pos] //只能访问不能修改
rope<int> *his[N]; his[i]=new rope<int>(*his[i-1]); //据说O(1)拷贝,一行可持久化
随机数
#include<random>
mt19937 rnd(time(0));
//巨佬定义:mt19937 rnd(chrono::high_resolution_clock::now().time_since_epoch().count());
cout<<rnd()<<endl; //范围是unsigned int
STL汇总
将第k+1小的数置于k这个位置:nth_element(begin,begin+k,end);
返回是否存在:binary_search(begin,end,key,[less])
返回限制最小值地址:upper_bound(begin,end,key,[less])
返回严格限制最小值地址:lower_bound(begin,end,key,[less])
填充:fill(begin,end,element);
递增填充(赋值为t,t+1,t+2,...):iota(begin,end,t);
复制(注意会复制到b里):copy(a_begin,a_end,b_begin);
翻转:reverse(begin,end);
单次归并排序(合并有序列表a,b至c):merge(a_begin,a_end,b_begin,b_end,c_begin,[less]);
归并排序(原地保存):inplace_merge(begin,begin+k,end);
vector去重(之前要排序):a.erase(unique(a.begin(),a.end()),a.end());
vector释放额外内存:a.shrink_to_fit();
并集(结果存c):set_union(a_begin,a_end,b_begin,b_end,c_begin);
O(n)建堆:priority_queue<int>(begin,end)
set,map查找,没找到返回end():a.find(key)
set,map限制最小值:a.lower_bound(key)
set,map合并:a.insert(b.begin(),b.end()); //O(nlogn)
complex<lf> c; complex<lf> c(1,2);//复数
c.real(),c.imag() //实部、虚部
bitset<32> b; //声明一个32位的bitset
b[n]; b[n]=1; //访问和修改
b.reset(); b.set(); //全部置0或置1
b.none(); //返回是否为空
b.count(); //返回1的个数
b.flip(); b.flip(n); //全反、反转第n位
手写hash
struct myhash{
ll f(ll x)const{
x+=0x9e3779b97f4a7c15;
x=(x^(x>>30))*0xbf58476d1ce4e5b9;
x=(x^(x>>27))*0x94d049bb133111eb;
return x^(x>>31);
}
ll operator()(int x)const{
static int t=chrono::steady_clock::now().time_since_epoch().count();
return f(x+t);
}
};
面向对象
struct vi:vector<int>{ //继承
mutable int x; //强制可修改,比如在set中可以直接修改
explicit vi(int){} //禁止隐式转换的构造函数
};
吸氧气
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
进制转换
itoa(n,str,base) //将n存入字符数组中(只能int)(不一定能用,cf就不行)
strtol(str,0,base),strtoll //返回字符数组的base进制数
stol(s,0,base),stoll //返回字符串的base进制数
sscanf,sprintf //十进制
to_string(n) //十进制
cmath
lround(x) //四舍五入到long
llround(x) //四舍五入到long long
fmod(x) //浮点取模
ilogb(x) //相当于(int)floor(log2(x))
tgamma(x) //计算Γ(x)
atan2(x,y) //计算坐标(x,y)的极角
hypot(x,y) //计算sqrt(x^2+y^2)
scanf字符串正则化
scanf("%ns",str); //读入n个字符
scanf("%[a-z]",str); //遇到非小写字母停止
scanf("%[^0-9]",str); //遇到数字停止,^是非
scanf("%*[a-z]"); //也是遇到非小写字母停止,只不过不读入字符串
双关键字比较
- 不如用
make_pair(a.x,a.y)<make_pair(b.x,b.y)
或者make_tuple
int
类型还可以pii(a.x,a.y)<pii(b.x,b.y)
- 如果都是左值的话好像
tie
也可以
参数包(直接从t神模板抄过来的,貌似没卵用)
void debug_out(){cerr<<endl;}
template<typename Head,typename... Tail>
void debug_out(Head H,Tail... T){
cerr<<" "<<H;
debug_out(T...);
}
#define debug(...) cerr<<"["#__VA_ARGS__"]:",debug_out(__VA_ARGS__)
神奇特性
- 命名空间rel_ops:之后只定义小于就能用其他所有次序关系符号
- raw strings:
R"(abc\n)"
相当于"abc\\n"
- 定义数字开头的变量:
type operator ""_name(type number){/*...*/}
(之后1_name
即把1带入上述函数中,参数类型只能是ull,llf,char,(const char *,size_t)
) - 高级宏:
__VA_ARGS__
是参数列表(对应...
),__LINE__
是当前行数,__FUNCTION__
是当前函数名,__COUNTER__
是宏展开次数-1 - vector类型的a赋值为b,b清空:
a=move(b);
java
import java.util.*;
import java.math.BigDecimal;
import java.math.BigInteger;
public class Main{
static Scanner sc;
public static void main(String[] args){
sc=new Scanner(System.in);
}
}
- 编译运行
java Main.java
- 编译
javac Main.java
//生成Main.class - 运行
java Main
数据类型
int //4字节有符号
long //8字节有符号
double
boolean
char //还能表示Unicode字符
String
final double PI=3.14; //final => (c++) const
var n=1; //var => (c++) auto
long
型常量结尾加L
,如1L
- 空指针
null
数组
int[] arr=new int[100]; //数组
int[] arr=new int[]{1,2,3}; //有初始值、不指定长度的声明
int[] arr={1,2,3}; //有初始值、不指定长度的声明
arr.length //获取数组长度
int[][] arr=new int[10][10]; //二维数组
Array.sort(arr,l,r); //对arr[l..(r-1)]排序(import java.util.Arrays;)
输出
System.out.print(x);
System.out.println();
System.out.println(x); //自动换行
System.out.printf("%.2f\n",d); //格式化:%d %f %s
输入
import java.util.Scanner;
Scanner sc=new Scanner(System.in); //初始化
String s=sc.nextline(); //读一行字符串
int n=sc.nextInt(); //读整数
double d=sc.nextDouble(); //读实数
sc.hasNext() //是否读完
if,switch,while,do while,for,for(i:A)同C++
String
不可变性:内部是常量
比较:s1.equals(s2)
是否包含子串s2:s1.contains(s2)
查找子串:s1.indexOf(s2)
反向查找子串:s1.lastIndexOf(s2)
获取子串:s1.substring(2,4) //首末坐标[2,4)
提取字符:s1.charAt(3) //就像c++的s1[3]
s1.equalsIgnoreCase(s2)
s1.startsWith(s2) //boolean
s1.endsWith(s2) //boolean
Math
//不用import就能用下列函数
Math.{sqrt,sin,atan,abs,max,min,pow,exp,log,PI,E}
Random
import java.util.Random;
Random rnd=new Random(); //已经把时间戳作为了种子
rnd.nextInt();
rnd.nextInt(n); //[0,n)
class
class node{
int l,r;
} //不用分号
//之后在class Main里定义static node name
BigInteger
import java.math.BigInteger;
BigInteger n=new BigInteger("0");
BigInteger[] arr=new BigInteger[10];
n.intValue() //转换为int
n.longValue() //转换
n.doubleValue() //转换
n.add(n2) //加法
n.subtract(n2) //减法
n.multiply(n2) //乘法
n.divide(n2) //除法
n.mod(n2) //取模
BigInteger.valueOf(I) //int转换为BigInteger
n.compareTo(n2) //n>n2返回1,n<n2返回-1,n==n2返回0
n.abs()
n.pow(I)
n.toString(I) //返回I进制字符串
//运算时n2一定要转换成BigInteger
BigDecimal
import java.math.BigDecimal;
n.divide(n2,2,BigDecimal.ROUND_HALF_UP) //保留两位(四舍五入)
//貌似没有sqrt等操作,都得自己实现qwq
暴力算法
离散化
- 从小到大标号并赋值,\(O(n\log n)\),
是个好东西
void disc(int a[],int n){
vector<int> b(a,a+n);
sort(b.begin(),b.end());
b.erase(unique(b.begin(),b.end()),b.end());
repeat(i,0,n)
a[i]=lower_bound(b.begin(),b.end(),a[i])-b.begin(); //从0开始编号
}
void disc(int a[],int n,int d){ //把距离>d的拉近到d
vector<int> b(a,a+n);
sort(b.begin(),b.end());
b.erase(unique(b.begin(),b.end()),b.end());
vector<int> c(b.size()); c[0]=0; //从0开始编号
repeat(i,1,b.size())
c[i]=c[i-1]+min(d,b[i]-b[i-1]);
repeat(i,0,n)
a[i]=c[lower_bound(b.begin(),b.end(),a[i])-b.begin()];
}
01分数规划
- \(n\) 个物品,都有两个属性 \(a_i\) 和 \(b_i\),任意取 \(k\) 个物品使它们的 \(\dfrac {\sum a_j}{\sum b_j}\) 最大
- 解:二分答案
- \(m\) 是否满足条件即判断 \(\dfrac {\sum a_j}{\sum b_j}\ge m\),即 \(\sum(a_j-mb_j)\ge 0\)
- 因此计算 \(c_i=a_i-mb_i\) ,取前 \(k\) 个最大值看它们之和是否 \(\ge 0\)
- 如果限制条件是 \(\sum b_j\ge W\),则将 \(b_j\) 看成体积,\(a_j-mb_j\) 看成价值,转换为背包dp
int n,k; lf a[N],b[N],c[N];
bool check(lf mid){
repeat(i,0,n)c[i]=a[i]-mid*b[i];
nth_element(c,c+k,c+n,greater<lf>());
lf sum=0; repeat(i,0,k)sum+=c[i];
return sum>=0;
}
lf solve(){
lf l=0,r=1;
while(r-l>1e-9){
lf mid=(l+r)/2;
if(check(mid))l=mid; else r=mid;
}
return l;
}
任务规划 | Livshits-Kladov定理
- 给出 \(n\) 个任务,第 \(i\) 个任务花费 \(t_i\) 时间,该任务开始之前等待 \(t\) 时间的代价是 \(f_i(t)\) 个数,求一个任务排列方式,最小化代价 \(\sum\limits_{i=1}^n f_j(\sum\limits_{j=1}^{i-1}t_i)\)
- Livshits-Kladov定理:当 \(f_i(t)\) 是一次函数 / 指数函数 / 相同的单增函数时,最优解可以用排序计算
- 一次函数:\(f_i(t)=c_it+d_i\),按 \(\dfrac {c_i}{t_i}\) 升序排列
- 指数函数:\(f_i(t)=c_ia^t+d_i\),按 \(\dfrac{1-a^{t_i}}{c_i}\) 升序排列
- 相同的单增函数:按 \(t_i\) 升序排序
分治
逆序数×二维偏序
- \(O(n\log n)\)
void merge(int l,int r){ //归并排序
//对[l,r-1]的数排序
if(r-l<=1)return;
int mid=l+(r-l)/2;
merge(l,mid);
merge(mid,r);
int p=l,q=mid,s=l;
while(s<r){
if(p>=mid || (q<r && a[p]>a[q])){
t[s++]=a[q++];
ans+=mid-p; //统计逆序数
}
else
t[s++]=a[p++];
}
for(int i=l;i<r;++i)a[i]=t[i];
}
最大空矩阵 | 悬线法
- 求01矩阵中全是0的最大连续子矩阵(面积最大)\(O(nm)\)
- 此处障碍物是正方形。如果障碍只是一些整点,答案从 \(ab\) 变为 \((a+1)(b+1)\)
int n,m,a[N][N],l[N][N],r[N][N],u[N][N];
int getlm(){
int ans=0;
repeat(i,0,n)
repeat(k,0,m)
l[i][k]=r[i][k]=u[i][k]=(a[i][k]==0);
repeat(i,0,n){
repeat(k,1,m)
if(a[i][k]==0)
l[i][k]=l[i][k-1]+1; //可以向左延伸几格
repeat_back(k,0,m-1)
if(a[i][k]==0)
r[i][k]=r[i][k+1]+1; //可以向右延伸几格
repeat(k,0,m)
if(a[i][k]==0){
if(i!=0 && a[i-1][k]==0){
u[i][k]=u[i-1][k]+1; //可以向上延伸几格
l[i][k]=min(l[i][k],l[i-1][k]);
r[i][k]=min(r[i][k],r[i-1][k]); //如果向上延伸u格,lr对应的修改
}
ans=max(ans,(l[i][k]+r[i][k]-1)*u[i][k]);
}
}
return ans;
}
搜索
舞蹈链×DLX
精确覆盖
- 在01矩阵中找到某些行,它们两两不相交,且它们的并等于全集
- xy编号从 \(1\) 开始!\(O(\exp)\),节点数 \(<5000\)
int n,m;
vector<int> rec; //dance后存所有选中的行的编号
struct DLX{
#define rep(i,i0,a) for(int i=a[i0];i!=i0;i=a[i])
int u[N],d[N],l[N],r[N],x[N],y[N]; //x[]行,y[]列,N=10010
int sz[N],h[N]; //一列几个元素,行首元素
int top;
void init(){
top=m;
repeat(i,0,m+1){
sz[i]=0;
u[i]=d[i]=i;
l[i]=i-1;
r[i]=i+1;
}
l[0]=m;
r[m]=0;
repeat(i,0,n+1)h[i]=-1;
rec.clear();
}
void add(int x0,int y0){ //添加(x0,y0)
top++;
sz[y0]++;
x[top]=x0;
y[top]=y0;
u[top]=u[y0];
d[top]=y0;
u[d[top]]=d[u[top]]=top;
if(h[x0]<0)
h[x0]=l[top]=r[top]=top;
else{
l[top]=h[x0];
r[top]=r[h[x0]];
l[r[h[x0]]]=top;
r[h[x0]]=top;
}
}
void remove(int c){ //删除列c中出现1的所有行
l[r[c]]=l[c];
r[l[c]]=r[c];
rep(i,c,d)
rep(j,i,r){
u[d[j]]=u[j];
d[u[j]]=d[j];
sz[y[j]]--;
}
}
void resume(int c){ //重置列c中出现1的所有行
rep(i,c,d)
rep(j,i,r){
u[d[j]]=d[u[j]]=j;
sz[y[j]]++;
}
l[r[c]]=r[l[c]]=c;
}
bool dance(int dep=1){ //返回是否可行
if(r[0]==0)return 1;
int c=r[0];
rep(i,0,r)
if(sz[c]>sz[i])
c=i; //选取出现1元素次数最少的一列进行删除(依次删除这一列有1的行)
remove(c);
rep(i,c,d){
rep(j,i,r)
remove(y[j]);
if(dance(dep+1)){rec.push_back(x[i]);return 1;}
rep(j,i,l)
resume(y[j]);
}
resume(c);
return 0;
}
}dlx;
重复覆盖
- 在01矩阵中找到最少的行,它们的并等于全集
- xy编号还是从 \(1\) 开始!\(O(\exp)\),节点数可能 \(<3000\)
struct DLX{
#define rep(i,d,s) for(node* i=s->d;i!=s;i=i->d)
struct node{
node *l,*r,*u,*d;
int x,y;
};
static const int M=2e5;
node pool[M],*h[M],*R[M],*pl;
int sz[M],vis[M],ans,clk;
void init(int n,int m){ //行和列
clk=0;
ans=inf;
pl=pool;
++m;
repeat(i,0,max(n,m)+1){
R[i]=0;
sz[i]=0;
vis[i]=-1;
}
repeat(i,0,m)
h[i]=new(pl++)node;
repeat(i,0,m){
h[i]->l=h[(i+m-1)%m];
h[i]->r=h[(i+1)%m];
h[i]->u=h[i]->d=h[i];
h[i]->y=i;
}
}
void link(int x,int y){
sz[y]++;
auto p=new(pl++)node;
p->x=x; p->y=y;
p->u=h[y]->u; p->d=h[y];
p->d->u=p->u->d=p;
if(!R[x])R[x]=p->l=p->r=p;
else{
p->l=R[x]; p->r=R[x]->r;
p->l->r=p->r->l=p;
}
}
void remove(node* p){
rep(i,d,p){
i->l->r=i->r;
i->r->l=i->l;
}
}
void resume(node* p){
rep(i,u,p)
i->l->r=i->r->l=i;
}
int eval(){
++clk;
int ret=0;
rep(i,r,h[0])
if(vis[i->y]!=clk){
++ret;
vis[i->y]=clk;
rep(j,d,i)
rep(k,r,j)
vis[k->y]=clk;
}
return ret;
}
void dfs(int d){
if(h[0]->r==h[0]){ans=min(ans,d); return;}
if(eval()+d>=ans)return;
node* c;
int m=inf;
rep(i,r,h[0])
if(sz[i->y]<m){m=sz[i->y]; c=i;}
rep(i,d,c){
remove(i);
rep(j,r,i)remove(j);
dfs(d+1);
rep(j,l,i)resume(j);
resume(i);
}
}
int solve(){ //返回最优解
ans=inf;
dfs(0);
return ans;
}
}dlx;
启发式算法
A-star
- 定义 \(g(v)\) 是 \(s\) 到 \(v\) 的实际代价,\(h(v)\) 是 \(v\) 到 \(t\) 的估计代价
- 定义估价函数 \(f(v)=g(v)+h(v)\)
- 每次从堆里取出 \(f(v)\) 最小的点进行更新
- 如果满足 \(h(v_1)+w(v_2,v_1)\ge h(v_2)\) (存疑)则不需要重复更新同一点,可以用set标记,已标记的不入堆
模拟退火
- 以当前状态为中心,半径为温度的圆内选一个点
- 计算
d=
当前最优解减去当前解 - 如果小于 \(0\) 则直接状态转移
- 否则状态转移的概率是
exp(-k*d/t)
- 最后温度乘以降温系数,返回第一步
- 需要调 \(3\) 个参数:初始温度,终止温度,降温系数(?)
由于退火退不出太令人绝望了(其实是太菜了),建议少用
动态规划
多重背包
- 二进制版,\(O(nV\log num)\),\(V\) 是总容量
int n,V; ll dp[N];
void push(int val,int v,int c){ //处理物品(价值=val,体积=v,个数=c)
for(int b=1;c;c-=b,b=min(b*2,c)){
ll dv=b*v,dval=b*val;
repeat_back(j,dv,V+1)
dp[j]=max(dp[j],dp[j-dv]+dval);
}
}
//初始化fill(dp,dp+V+1,0),结果是dp[V]
- 单调队列版,\(O(nV)\),\(V\) 是总容量
int n,V; ll dp[N];
void push(int val,int v,int c){ //处理物品(价值=val,体积=v,个数=c)
static deque< pair<int,ll> > q; //单调队列,fi是位置,se是价值
if(v==0){
repeat(i,0,V+1)dp[i]+=val*c;
return;
}
c=min(c,V/v);
repeat(d,0,v){
q.clear();
repeat(j,0,(V-d)/v+1){
ll t=dp[d+j*v]-j*val;
while(!q.empty() && t>=q.back().se)
q.pop_back();
q.push_back({j,t});
while(q.front().fi<j-c)
q.pop_front();
dp[d+j*v]=max(dp[d+j*v],q.front().se+j*val);
}
}
}
//初始化fill(dp,dp+V+1,0),结果是dp[V]
最长不降子序列×LIS
- 二分查找优化,\(O(n\log n)\)
const int inf=1e9;
repeat(i,0,n+1)dp[i]=inf; //初始化为inf
repeat(i,0,n)
*lower_bound(dp,dp+n,a[i])=a[i];
return lower_bound(dp,dp+n,inf)-dp;
数位dp
- 记忆化搜索,dfs的参数lim表示是否被限制,lz表示当前位的前一位是不是前导零
- 复杂度等于状态数
- 如果每个方案贡献不是1,dp可能要变成struct数组(cnt,sum,....)
ll dp[20][*],bit[20];
ll dfs(int pos,ll *,bool lim=1,bool lz=1){
if(pos==-1)return *; //返回该状态是否符合要求(0或1)
ll &x=dp[pos][*];
if(!lim && x!=-1)return x;
ll ans=0;
int maxi=lim?bit[pos]:9;
repeat(i,0,maxi+1){
...//状态转移
if(lz && i==0)...//可能要用lz,其他地方都不用
ans+=dfs(pos-1,*,
lim && i==maxi,
lz && i==0);
}
if(!lim)x=ans; //不限制的时候才做存储
return ans;
}
ll solve(ll n){
int len=0;
while(n)bit[len++]=n%10,n/=10;
return dfs(len-1,*);
}
signed main(){
mst(dp,-1); //在很多时候dp值可以反复使用
ll t=read();
while(t--){
ll l=read(),r=read();
printf("%lld\n",solve(r)-solve(l-1));
}
return 0;
}
换根dp
- 两次dfs
- 第一次求所有点所在子树的答案 \(dp_v\),此时 \(dp_{rt}\) 是 \(rt\) 的最终答案
- 第二次将根转移来算其他点的最终答案,回溯时复原即可
void dfs1(int x,int fa=-1){
for(auto p:a[x])
if(p!=fa){
dfs1(p,x);
dp[x]+=op(dp[p]);
}
}
void dfs2(int x,int fa=-1){
ans[x]=dp[x];
for(auto p:a[x])
if(p!=fa){
dp[x]-=op(dp[p]);
dp[p]+=op(dp[x]);
dfs2(p,x);
dp[p]-=op(dp[x]);
dp[x]+=op(dp[p]);
}
}
斜率优化
- 例:HDOJ3507
- \(dp_i=\min\limits_{j∈[0,i)}[dp_j+(s_i-s_j)^2+M]\)
- 考虑 \(k<j<i\),\(j\) 决策优于 \(k\Leftrightarrow dp_j+(s_i-s_j)^2<dp_k+(s_i-s_k)^2\)
- 一通操作后 \(\dfrac{s_j^2+dp_j-s_k^2-dp_k}{2(s_j-s_k)}<s_i\),即寻找点集 \(\{(2s_j,s_j^2+dp_j)\}\) 的下凸包中斜率刚好 \(>s_i\) 的线段的左端点,作为决策点
四边形优化
- \(dp(l,r)=\min\limits_{k∈[l,r)}[dp(l,k)+dp(k+1,r)]+w(l,r)\)
- 其中 \(w(l,r)\) 满足
- 区间包含单调性:任意 \(l \le l' \le r' \le r\) 有 \(w(l',r')\le w(l,r)\)
- 四边形不等式:任意 \(a \le b \le c \le d\) 有 \(w(a,c)+w(b,d)\le w(a,d)+w(b,c)\)(若等号恒成立则满足四边形恒等式)
- 决策单调性:令 \(m(l,r)\) 为最优决策点(满足 \(dp(l,r)=dp(l,m)+dp(m+1,r)+w(l,r)\)),则有 \(m(l,r-1) \le m(l,r) \le m(l+1,r)\),遍历这个区间可以优化至 \(O(n^2)\)
repeat(i,0,n)dp[i][i]=0,m[i][i]=i;
repeat(len,2,n+1)
for(int l=0,r=len-1;r<n;l++,r++){
dp[l][r]=inf;
repeat(k,m[l][r-1],min(m[l+1][r]+1,r))
if(dp[l][r]>dp[l][k]+dp[k+1][r]+w(l,r)){
dp[l][r]=dp[l][k]+dp[k+1][r]+w(l,r);
m[l][r]=k;
}
}
- \(dp(i)=\min\limits_{k∈[1,i)}w(k,i)\),\(w(l,r)\) 满足四边形不等式
- 决策单调性:令 \(m_i\) 为最优决策点(满足 \(dp(i)=w(m,i)\)),则 \(m_{i-1}\le m_i\),因此可以分治优化成 \(O(n\log n)\)
计算几何
struct of 向量
struct vec{
lf x,y;
vec(lf x=0,lf y=0):x(x),y(y){}
vec operator-(const vec &b){return vec(x-b.x,y-b.y);}
vec operator+(const vec &b){return vec(x+b.x,y+b.y);}
vec operator*(lf k){return vec(k*x,k*y);}
lf len(){return hypot(x,y);}
lf sqr(){return x*x+y*y;}
/*截取*/vec trunc(lf k=1){return *this*(k/len());}
/*逆时针旋转*/vec rotate(double th){lf c=cos(th),s=sin(th); return vec(x*c-y*s,x*s+y*c);}
}a[N];
lf cross(vec a,vec b){return a.x*b.y-a.y*b.x;};
lf cross(vec a,vec b,vec c){return cross(a-b,b-c);}
lf dot(vec a,vec b){return a.x*b.x+a.y*b.y;}
bool cmp_xy(const vec &a,const vec &b){return make_pair(a.x,a.y)<make_pair(b.x,b.y);}
bool cmp_atan(const vec &a,const vec &b){return atan2(a.x,a.y)<atan2(b.x,b.y);}
/*输出*/ostream &operator<<(ostream &o,const vec &v){return o<<'('<<v.x<<','<<v.y<<')';}
平面几何基本操作
判断两条线段是否相交
- 快速排斥实验:判断线段所在矩形是否相交(用来减小常数,可省略)
- 跨立实验:任一线段的两端点在另一线段的两侧
bool judge(vec a,vec b,vec c,vec d){ //线段ab和线段cd
#define SJ(x) max(a.x,b.x)<min(c.x,d.x)\
|| max(c.x,d.x)<min(a.x,b.x)
if(SJ(x) || SJ(y))return 0;
#define SJ2(a,b,c,d) cross(a-b,a-c)*cross(a-b,a-d)<=0
return SJ2(a,b,c,d) && SJ2(c,d,a,b);
}
others of 平面几何基本操作
点是否在线段上
bool onseg(vec p,vec a,vec b){
return (a.x-p.x)*(b.x-p.x)<err
&& (a.y-p.y)*(b.y-p.y)<err
&& fabs(cross(a-b,a-p))<err;
}
多边形面积
lf area(vec a[],int n){
lf ans=0;
repeat(i,0,n)
ans+=cross(a[i],a[(i+1)%n]);
return fabs(ans/2);
}
多边形的面积质心
vec centre(vec a[],int n){
lf S=0;
vec v=vec();
repeat(i,0,n){
vec &v1=a[i],&v2=a[(i+1)%n];
lf s=cross(v1,v2);
S+=s;
v=v+(v1+v2)*s;
}
return v*(1/(3*S));
}
二维凸包
- 求上凸包,按坐标 \((x,y)\) 字典升序排序,从小到大加入栈,如果出现凹多边形情况则出栈。下凸包反着来
- \(O(n\log n)\),排序是瓶颈
//struct vec{构造函数,减法,小于};
lf cross(vec a,vec b){return a.x*b.y-a.y*b.x;}
lf cross(vec a,vec b,vec c){return cross(a-b,b-c);}
vector<vec> st;
void push(vec &v,int b){
while(st.size()>b
&& cross(*++st.rbegin(),st.back(),v)<=0) //会得到逆时针的凸包
st.pop_back();
st.push_back(v);
}
void convex(vec a[],int n){
st.clear();
sort(a,a+n);
repeat(i,0,n)push(a[i],1);
int b=st.size();
repeat_back(i,1,n-1)push(a[i],b);
//repeat_back自动变成上凸包
}
旋转卡壳
- 每次找到凸包每条边的最远点,基于二维凸包,\(O(n\log n)\)
lf calipers(vec a[],int n){
convex(a,n); //凸包算法
repeat(i,0,st.size())a[i]=st[i]; n=st.size();
lf ans=0;
int p=1;
a[n]=a[0];
repeat(i,0,n){
while(cross(a[p],a[i],a[i+1])<cross(a[p+1],a[i],a[i+1])) //必须逆时针凸包
p=(p+1)%n;
ans=max(ans,(a[p]-a[i]).len());
ans=max(ans,(a[p+1]-a[i]).len()); //这里求了直径
}
return ans;
}
最大空矩形 | 扫描法
- 在范围 \((0,0)\) 到 \((l,w)\) 内求面积最大的不覆盖任何点的矩形面积,\(O(n^2)\),\(n\) 是点数
- 如果是
lf
就把vec
结构体内部、ans
、u
和d
的类型改一下
struct vec{
int x,y; //可能是lf
vec(int x,int y):x(x),y(y){}
};
vector<vec> a; //存放点
int l,w;
int ans=0;
void work(int i){
int u=w,d=0;
repeat(k,i+1,a.size())
if(a[k].y>d && a[k].y<u){
ans=max(ans,(a[k].x-a[i].x)*(u-d)); //更新ans
if(a[k].y==a[i].y)return; //可行性剪枝
(a[k].y>a[i].y?u:d)=a[k].y; //更新u和d
if((l-a[i].x)*(u-d)<=ans)return; //最优性剪枝
}
ans=max(ans,(l-a[i].x)*(u-d)); //撞墙更新ans
}
int query(){
a.push_back(vec(0,0));
a.push_back(vec(l,w)); //加两个点方便处理
//小矩形的左边靠着顶点的情况
sort(a.begin(),a.end(),[](vec a,vec b){return a.x<b.x;});
repeat(i,0,a.size())
work(i);
//小矩形的右边靠着顶点的情况
repeat(i,0,a.size())a[i].x=l-a[i].x; //水平翻折
sort(a.begin(),a.end(),[](vec a,vec b){return a.x<b.x;});
repeat(i,0,a.size())
work(i);
//小矩形左右边都不靠顶点的情况
sort(a.begin(),a.end(),[](vec a,vec b){return a.y<b.y;});
repeat(i,0,(int)a.size()-1)
ans=max(ans,(a[i+1].y-a[i].y)*l);
return ans;
}
平面最近点对 | 分治
- \(O(n\log n)\)
lf ans;
bool cmp_y(vec a,vec b){return a.y<b.y;}
void rec(int l,int r){ //左闭右开区间
#define upd(x,y) {ans=min(ans,(x-y).len());}
if(r-l<4){
repeat(i,l,r)
repeat(j,i+1,r)
upd(a[i],a[j]);
sort(a+l,a+r,cmp_y); //按y排序
return;
}
int m=(l+r)/2;
lf midx=a[m].x;
rec(l,m),rec(m,r);
static vec b[N];
merge(a+l,a+m,a+m,a+r,b+l,cmp_y); //逐渐按y排序
copy(b+l,b+r,a+l);
int t=0;
repeat(i,l,r)
if(fabs(a[i].x-midx)<ans){
repeat_back(j,0,t){
if(a[i].y-b[i].y>ans)break;
upd(a[i],b[j]);
}
b[t++]=a[i];
}
}
lf nearest(){
ans=1e20;
sort(a,a+n); //按x排序
rec(0,n);
return ans;
}
最小圆覆盖 | 随机增量法
- eps可能要非常小。随机化,均摊 \(O(n)\)
struct cir{ //圆(结构体)
vec v; lf r;
bool out(vec a){ //点a在圆外
return (v-a).len()>r+err;
}
cir(vec a){ //点圆
v=a;
r=0;
}
cir(vec a,vec b){ //确定直径的圆
v=(a+b)*0.5;
r=(v-a).len();
}
cir(vec a,vec b,vec c){ //三个点的外接圆
b=b-a,c=c-a;
vec s=vec(b.sqr(),c.sqr())*0.5;
lf d=1/cross(b,c);
v=a+vec(s.x*c.y-s.y*b.y,s.y*b.x-s.x*c.x)*d;
r=(v-a).len();
}
};
cir RIA(vec *a,int n){ //RIA=随机增量法
repeat_back(i,2,n)swap(a[rand()%i],a[i]); //random_shuffle(a,a+n);
cir c=cir(a[0]);
repeat(i,1,n)
if(c.out(a[i])){
c=cir(a[i]);
repeat(j,0,i)
if(c.out(a[j])){
c=cir(a[i],a[j]);
repeat(k,0,j)
if(c.out(a[k]))
c=cir(a[i],a[j],a[k]);
}
}
return c;
}
struct of 三维向量
struct vec{
lf x,y,z;
vec(lf x=0,lf y=0):x(x),y(y){};
vec operator-(vec b){return vec(x-b.x,y-b.y,z-b.z);}
vec operator+(vec b){return vec(x+b.x,y+b.y,z+b.z);}
vec operator*(lf k){return vec(k*x,k*y,k*z);}
bool operator<(vec b)const{return make_tuple(x,y,z)<make_pair(b.x,b.y,b.z);}
lf len(){return sqrt(x*x+y*y+z*z);}
}a[N];
vec cross(vec a,vec b){
return vec(
a.y*b.z-a.z*b.y,
a.z*b.x-a.x*b.z,
a.x*b.y-a.y*b.x);
}
lf dot(vec a,vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}
三维凸包
- 将所有凸包上的面放入面集
f
中,其中face::p[i]
作为a
的下标,\(O(n^2)\)
const lf err=1e-9;
struct vec{
lf x,y,z;
vec(lf x=0,lf y=0,lf z=0):x(x),y(y),z(z){};
vec operator-(vec b){return vec(x-b.x,y-b.y,z-b.z);}
lf len(){return sqrt(x*x+y*y+z*z);}
void shake(){ //微小扰动
x+=(rand()*1.0/RAND_MAX-0.5)*err;
y+=(rand()*1.0/RAND_MAX-0.5)*err;
z+=(rand()*1.0/RAND_MAX-0.5)*err;
}
}a[N];
vec cross(vec a,vec b){
return vec(
a.y*b.z-a.z*b.y,
a.z*b.x-a.x*b.z,
a.x*b.y-a.y*b.x);
}
lf dot(vec a,vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}
struct face{
int p[3];
vec normal(){ //法向量
return cross(a[p[1]]-a[p[0]],a[p[2]]-a[p[0]]);
}
lf area(){return normal().len()/2.0;}
};
vector<face> f;
bool see(face f,vec v){
return dot(v-a[f.p[0]],f.normal())>0;
}
void convex(vec a[],int n){
static vector<face> c;
static bool vis[N][N];
repeat(i,0,n)a[i].shake(); //防止四点共面
f.clear();
f.push_back((face){0,1,2});
f.push_back((face){0,2,1});
repeat(i,3,n){
c.clear();
repeat(j,0,f.size()){
bool t=see(f[j],a[i]);
if(!t) //加入背面
c.push_back(f[j]);
repeat(k,0,3){
int x=f[j].p[k],y=f[j].p[(k+1)%3];
vis[x][y]=t;
}
}
repeat(j,0,f.size())
repeat(k,0,3){
int x=f[j].p[k],y=f[j].p[(k+1)%3];
if(vis[x][y] && !vis[y][x]) //加入新面
c.push_back((face){x,y,i});
}
f.swap(c);
}
}
计算几何杂项
正幂反演
- 给定反演中心 \(O\) 和反演半径 \(R\)。若直线上的点 \(OPQ\) 满足 \(|OP|\cdot|OQ|=R^2\),则 \(P\) 和 \(Q\) 互为反演点
- 不经过反演中心的圆的反演图形是圆(计算时取圆上靠近/远离中心的两个点)
- 经过反演中心的圆的反演图形是直线(计算时取远离中心的点,做垂线)
others of 计算几何杂项
曼哈顿、切比雪夫距离
- 曼:
mdist=|x1-x2|+|y1-y2|
- 切:
cdist=max(|x1-x2|,|y1-y2|)
- 转换:
mdist((x,y),*)=cdist((x+y,x-y),**)
cdist((x,y),*)=mdist(((x+y)/2,(x-y)/2),**)
Pick定理
- 正方形点阵:
面积 = 内部点数 + 边上点数 / 2 - 1
- 三角形点阵:
面积 = 2 * 内部点数 + 边上点数 - 2
四面体体积公式
- (棱 \(a,b,c\) 共顶点)
- \(V^2=\dfrac 1 {288} \left|\left|\begin{array}{c} 0&a^2&b^2&c^2&1\\ a^2&0&c_1^2&b_1^2&1\\ b^2&c_1^2&0&a_1^2&1\\ c^2&b_1^2&a_1^2&0&1\\ 1&1&1&1&0\\ \end{array}\right|\right|\)
四元数与旋转
- 若要让点 \(p=xi+yj+zk\) 绕轴 \(l=ai+bj+ck\)(单位向量)转 \(\theta\) 弧度,可以让 \(p\) 左乘 \(q=\cos\dfrac \theta 2+(ai+bj+ck)\sin\dfrac \theta 2\),右乘 \(q^{-1}=\cos\dfrac \theta 2-(ai+bj+ck)\sin\dfrac \theta 2\)
数据结构
st表
普通st表
- 编号从 \(0\) 开始,初始化 \(O(n\log n)\) 查询 \(O(1)\)
struct ST{
#define logN 21
#define U(x,y) max(x,y)
ll a[N][logN];
void init(int n){
repeat(i,0,n)
a[i][0]=in[i];
repeat(k,1,logN)
repeat(i,0,n-(1<<k)+1)
a[i][k]=U(a[i][k-1],a[i+(1<<(k-1))][k-1]);
}
ll query(int l,int r){
int s=31-__builtin_clz(r-l+1);
return U(a[l][s],a[r-(1<<s)+1][s]);
}
}st;
二维st表
- 编号从 \(0\) 开始,初始化 \(O(nm\log n\log m)\) 查询 \(O(1)\)
struct ST{ //注意logN=log(N)+2
#define logN 9
#define U(x,y) max(x,y)
int f[N][N][logN][logN],log[N];
ST(){
log[1]=0;
repeat(i,2,N)
log[i]=log[i/2]+1;
}
void build(){
repeat(k,0,logN)
repeat(l,0,logN)
repeat(i,0,n-(1<<k)+1)
repeat(j,0,m-(1<<l)+1){
int &t=f[i][j][k][l];
if(k==0 && l==0)t=a[i][j];
else if(k)
t=U(f[i][j][k-1][l],f[i+(1<<(k-1))][j][k-1][l]);
else
t=U(f[i][j][k][l-1],f[i][j+(1<<(l-1))][k][l-1]);
}
}
int query(int x0,int y0,int x1,int y1){
int k=log[x1-x0+1],l=log[y1-y0+1];
return U(U(U(
f[x0][y0][k][l],
f[x1-(1<<k)+1][y0][k][l]),
f[x0][y1-(1<<l)+1][k][l]),
f[x1-(1<<k)+1][y1-(1<<l)+1][k][l]);
}
}st;
<补充>猫树
- 编号从 \(0\) 开始,初始化 \(O(n\log n)\) 查询 \(O(1)\)
struct cat{
#define U(a,b) max(a,b) //查询操作
#define a0 0 //查询操作的零元
#define logN 21
vector<ll> a[logN];
vector<ll> v;
void init(){
repeat(i,0,logN)a[i].clear();
v.clear();
}
void push(ll in){
v.push_back(in);
int n=v.size()-1;
repeat(s,1,logN){
int len=1<<s; int l=n/len*len;
if(n%len==len/2-1){
repeat(i,0,len)a[s].push_back(a0);
repeat_back(i,0,len/2)a[s][l+i]=U(a[s][l+i+1],v[l+i]);
}
if(n%len>=len/2)
a[s][n]=(U(a[s][n-1],v[n]));
}
}
ll query(int l,int r){ //区间查询
if(l==r)return v[l];
int s=32-__builtin_clz(l^r);
return U(a[s][l],a[s][r]);
}
}tr;
单调队列
- 求所有长度为k的区间中的最小值,完整跑下来 \(O(n)\)
struct MQ{ //查询就用mq.q.front().first
deque<pii> q; //first:保存的最小值; second:时间戳
int T;
void init(){
q.clear();
T=0;
}
void push(int n){
T++;
while(!q.empty() && q.back().first>=n) //min
q.pop_back();
q.push_back({n,T});
while(!q.empty() && q.front().second<=T-k)
q.pop_front();
}
}mq;
树状数组
普通树状数组
- 单点+区间,修改查询 \(O(\log n)\)
#define lb(x) (x&(-x))
struct BIT{
ll t[N]; //一倍内存吧
void init(){
mst(t,0);
}
void add(ll x,ll k){ //位置x加上k
//x++;
for(;x<N;x+=lb(x))
t[x]+=k;
}
ll sum(ll x){ //求[1,x]的和 //[0,x]
//x++;
ll ans=0;
for(;x!=0;x-=lb(x))
ans+=t[x];
return ans;
}
}bit;
超级树状数组
- 基于树状数组,基本只允许加法,区间+区间,\(O(\log n)\)
struct SPBIT{
BIT a,a1;
void init(){a.init();a1.init();}
void add(ll x,ll y,ll k){
a.add(x,k);
a.add(y+1,-k);
a1.add(x,k*(x-1));
a1.add(y+1,-k*y);
}
ll sum(ll x,ll y){
return y*a.sum(y)-(x-1)*a.sum(x-1)-(a1.sum(y)-a1.sum(x-1));
}
}spbit;
二维超级树状数组
- 修改查询 \(O(\log n\cdot\log m)\)
int n,m;
#define lb(x) (x&(-x))
struct BIT{
ll t[N][N]; //一倍内存吧
void init(){
mst(t,0);
}
void add(int x,int y,ll k){ //位置(x,y)加上k
//x++,y++; //如果要从0开始编号
for(int i=x;i<=n;i+=lb(i))
for(int j=y;j<=m;j+=lb(j))
t[i][j]+=k;
}
ll sum(int x,int y){ //求(1..x,1..y)的和
//x++,y++; //如果要从0开始编号
ll ans=0;
for(int i=x;i!=0;i-=lb(i))
for(int j=y;j!=0;j-=lb(j))
ans+=t[i][j];
return ans;
}
};
struct SPBIT{
BIT a,ax,ay,axy;
void add(int x,int y,int k){
a.add(x,y,k);
ax.add(x,y,k*x);
ay.add(x,y,k*y);
axy.add(x,y,k*x*y);
}
ll sum(int x,int y){
return a.sum(x,y)*(x*y+x+y+1)
-ax.sum(x,y)*(y+1)
-ay.sum(x,y)*(x+1)
+axy.sum(x,y);
}
void add(int x0,int y0,int x1,int y1,int k){ //区间修改
add(x0,y0,k);
add(x0,y1+1,-k);
add(x1+1,y0,-k);
add(x1+1,y1+1,k);
}
ll sum(int x0,int y0,int x1,int y1){ //区间查询
return sum(x1,y1)
-sum(x0-1,y1)
-sum(x1,y0-1)
+sum(x0-1,y0-1);
}
}spbit;
线段树
数组版
- 区间+区间,修改查询 \(O(\log n)\)
struct seg{ //init(1,L,R) update(1,L,R,?,?,?) query(1,L,R,?,?)
#define U(x,y) (x+y) //查询运算
#define a0 0 //查询运算的零元
#define z0 0 //修改运算的零元
#define toz(n,x) z[n]+=x //加载到懒标记
#define toa() a[n]+=z[n]*(r-l+1),z[n]=z0 //懒标记加载到数据(z别忘了清空)
#define lc n*2
#define rc n*2+1
#define LC lc,l,(l+r)/2
#define RC rc,(l+r)/2+1,r
ll a[N*4],z[N*4]; //四倍内存(其实是大于N的最小的2的整数次幂的两倍)
void up(int n){
a[n]=U(a[lc],a[rc]);
}
void init(int n,int l,int r){
z[n]=z0;
if(l==r){
a[n]=in[l];
return;
}
init(RC);
init(LC);
up(n);
}
void down(int n,int l,int r){
if(l<r){
toz(lc,z[n]);
toz(rc,z[n]);
}
toa();
}
void update(int n,int l,int r,int x,int y,ll k){
x=max(x,l); y=min(y,r); if(x>y){down(n,l,r); return;}
if(x==l && y==r){
toz(n,k);
down(n,l,r);
return;
}
down(n,l,r);
update(LC,x,y,k);
update(RC,x,y,k);
up(n);
}
ll query(int n,int l,int r,int x,int y){
x=max(x,l); y=min(y,r); if(x>y)return a0;
down(n,l,r);
if(x==l && y==r)
return a[n];
return U(query(LC,x,y),query(RC,x,y));
}
}tr;
指针版
- l,r,lc,rc有点占内存
- 基本上适用于所有(线段树能实现的)区间+区间
- 我删了修改运算的零元,加了偷懒状态(state),
终于能支持赋值操作.jpg
struct seg{ //初始化init()修改查询tr->sth()
#define U(x,y) (x+y) //查询运算
#define a0 0 //查询运算的零元
void toz(ll x){z+=x,state=1;} //加载到懒标记
void toa(){a+=z*(r-l+1),z=0,state=0;} //懒标记加载到数据(z别忘了清空)
ll a,z; bool state; //数据,懒标记,是否偷了懒
int l,r;
seg *lc,*rc;
void init(int,int);
void up(){
a=U(lc->a,rc->a);
}
void down(){
if(!state)return;
if(l<r){
lc->toz(z);
rc->toz(z);
}
toa();
}
void update(int x,int y,ll k){
x=max(x,l); y=min(y,r); if(x>y){down();return;}
if(x==l && y==r){
toz(k);
down();
return;
}
down();
lc->update(x,y,k);
rc->update(x,y,k);
up();
}
ll query(int x,int y){
x=max(x,l); y=min(y,r); if(x>y)return a0;
down();
if(x==l && y==r)
return a;
return U(lc->query(x,y),rc->query(x,y));
}
}tr[N*2],*pl;
void seg::init(int _l,int _r){
l=_l,r=_r;
state=0;
if(l==r){
a=in[l];
return;
}
int m=(l+r)>>1;
lc=++pl; lc->init(l,m);
rc=++pl; rc->init(m+1,r);
up();
}
void init(int l,int r){
pl=tr;
tr->init(l,r);
}
<补充>zkw线段树
- 单点+区间,编号从0开始,建树 \(O(n)\) 修改查询 \(O(\log n)\)
- 代码量和常数都和树状数组差不多
struct seg{
#define U(a,b) max(a,b) //查询操作
ll a0=0; //查询操作的零元
int n; ll a[1024*1024*4*2]; //内存等于2^k且大于等于两倍inn
void init(int inn){ //建树
for(n=1;n<inn;n<<=1); repeat(i,inn,n)in[i]=a0;
repeat(i,0,n)a[n+i]=in[i];
repeat_back(i,1,n)up(i);
}
void up(int x){
a[x]=U(a[x<<1],a[(x<<1)^1]);
}
void update(int x,ll k){ //位置x加上k
a[x+=n]+=k; //也可以赋值等操作
while(x>>=1)up(x);
}
ll query(int l,int r){ //区间查询
ll ans=a0;
for(l+=n-1,r+=n+1;l^r^1;l>>=1,r>>=1){
if(~l & 1)ans=U(ans,a[l^1]); //l^1其实是l+1
if(r & 1)ans=U(ans,a[r^1]); //r^1其实是r-1
}
return ans;
}
}tr;
<补充>李超线段树
- 支持插入线段、查询所有线段与 \(x=x_0\) 交点最高的那条线段
- 修改 \(O(\log^2n)\),查询 \(O(\log n)\)
int funx; //这是y()的参数
struct func{
lf k,b; int id;
lf y()const{return k*funx+b;} //funx点处的高度
bool operator<(const func &b)const{
return make_pair(y(),-id)<make_pair(b.y(),-b.id);
}
};
struct seg{ //初始化init()更新update()查询query(),func::y()是高度
func a;
int l,r;
seg *ch[2];
void init(int,int);
void push(func d){
funx=(l+r)/2;
if(a<d)swap(a,d); //这个小于要用funx
if(l==r)return;
ch[d.k>a.k]->push(d);
}
void update(int x,int y,const func &d){ //更新[x,y]区间
x=max(x,l); y=min(y,r); if(x>y)return;
if(x==l && y==r)push(d);
else{
ch[0]->update(x,y,d);
ch[1]->update(x,y,d);
}
}
const func &query(int x){ //询问
funx=x;
if(l==r)return a;
const func &b=ch[(l+r)/2<x]->query(x);
return max(a,b); //这个max要用funx
}
}tr[N*2],*pl;
void seg::init(int _l,int _r){
l=_l,r=_r; a={0,-inf,-1}; //可能随题意改变
if(l==r)return;
int m=(l+r)/2;
(ch[0]=++pl)->init(l,m);
(ch[1]=++pl)->init(m+1,r);
}
void init(int l,int r){
pl=tr;
tr->init(l,r);
}
void add(int x0,int y0,int x1,int y1){ //线段处理并更新
if(x0>x1)swap(x0,x1),swap(y0,y1);
lf k,b;
if(x0==x1)k=0,b=max(y0,y1);
else{
k=lf(y1-y0)/(x1-x0);
b=y0-k*x0;
}
id++;
tr->update(x0,x1,{k,b,id});
}
并查集
- 合并查找 \(O(α(n))\),可视为 \(O(1)\)
普通并查集
- 精简版,只有路径压缩
struct DSU{ //合并:d[x]=d[y],查找:d[x]==d[y]
int a[N];
void init(int n){iota(a,a+n+1,0);}
int fa(int x){
return a[x]==x?x:a[x]=fa(a[x]);
}
int &operator[](int x){
return a[fa(x)];
}
}d;
- 普通版,路径压缩+按秩合并
struct DSU{
int a[10010],sz[10010];
void init(int n){
iota(a,a+n+1,0);
fill(sz,sz+n+1,1);
}
int fa(int x){
return a[x]==x?x:a[x]=fa(a[x]);
}
bool query(int x,int y){ //查找
return fa(x)==fa(y);
}
void join(int x,int y){ //合并
x=fa(x),y=fa(y);
if(x==y)return;
if(sz[x]>sz[y])swap(x,y);
a[x]=y;
sz[y]+=sz[x];
}
}d;
种类并查集
struct DSU{
int a[50010],r[50010];
void init(int n){
repeat(i,0,n+1)a[i]=i,r[i]=0;
}
int plus(int a,int b){ //关系a+关系b,类似向量相加
if(a==b)return -a;
return a+b;
}
int inv(int a){ //关系a的逆
return -a;
}
int fa(int x){ //返回根节点
if(a[x]==x)return x;
int f=a[x],ff=fa(f);
r[x]=plus(r[x],r[f]);
return a[x]=ff;
}
bool query(int x,int y){ //是否存在关系
return fa(x)==fa(y);
}
int getr(int x,int y){ //查找关系
return plus(r[x],inv(r[y]));
}
void join(int x,int y,int r2){ //按r2关系合并
r2=plus(plus(inv(r[x]),r2),r[y]);
x=fa(x),y=fa(y);
a[x]=y,r[x]=r2;
}
}d;
左偏树
- 万年不用,\(O(?)\)
如果没有特殊要求一律平板电视
struct leftist{ //编号从1开始,因为空的左右儿子会指向0
#define lc LC[x]
#define rc RC[x]
vector<int> val,dis,exist,dsu,LC,RC;
void init(){add(0);dis[0]=-1;}
void add(int v){
int t=val.size();
val.pb(v);
dis.pb(0);
exist.pb(1);
dsu.pb(t);
LC.pb(0);
RC.pb(0);
}
int top(int x){
return dsu[x]==x?x:dsu[x]=top(dsu[x]);
}
void join(int x,int y){
if(exist[x] && exist[y] && top(x)!=top(y))
merge(top(x),top(y));
}
int merge(int x,int y){
if(!x || !y)return x+y;
if(val[x]<val[y]) //大根堆
swap(x,y);
rc=merge(rc,y);
if(dis[lc]<dis[rc])
swap(lc,rc);
dsu[lc]=dsu[rc]=dsu[x]=x;
dis[x]=dis[rc]+1;
return x;
}
void pop(int x){
x=top(x);
exist[x]=0;
dsu[lc]=lc;
dsu[rc]=rc;
dsu[x]=merge(lc,rc); //指向x的dsu也能正确指向top
}
#undef lc
#undef rc
}lt;
//添加元素lt.add(v),位置是lt.val.size()-1
//是否未被pop:lt.exist(x)
//合并:lt.join(x,y)
//堆顶:lt.val[lt.top(x)]
//弹出:lt.pop(x)
珂朵莉树×老司机树
- 珂朵莉数以区间形式存储数据,
非常暴力,适用于有区间赋值操作的题 - 均摊 \(O(n\log\log n)\),但是
很可能被卡
struct ODT{
struct node{
int l,r;
mutable int v; //强制可修改
bool operator<(const node &b)const{return l<b.l;}
};
set<node> a;
void init(){ //初始化
a.clear();
a.insert({-inf,inf,0});
}
set<node>::iterator split(int x){ //分裂区间
auto it=--a.upper_bound({x,0,0});
if(it->l==x)return it;
int l=it->l,r=it->r,v=it->v;
a.erase(it);
a.insert({l,x-1,v});
return a.insert({x,r,v}).first;
}
void assign(int l,int r,int v){ //区间赋值
auto y=split(r+1),x=split(l);
a.erase(x,y);
a.insert({l,r,v});
}
int sum(int l,int r){ //操作示例:区间求和
auto y=split(r+1),x=split(l);
int ans=0;
for(auto i=x;i!=y;i++){
ans+=(i->r-i->l+1)*i->v;
}
return ans;
}
}odt;
K-D tree
- 例题:luogu P4148
- 支持在线在(x,y)处插入值、查询二维区间和
- 插入、查询 \(O(\log n)\)
struct node{
int x,y,v;
}s[N];
bool cmp1(int a,int b){return s[a].x<s[b].x;}
bool cmp2(int a,int b){return s[a].y<s[b].y;}
struct kdtree{
int rt,cur; //rt根节点
int d[N],sz[N],lc[N],rc[N]; //d=1竖着砍,sz子树大小
int L[N],R[N],D[N],U[N]; //该子树的界线
int sum[N]; //维护的二维区间信息(二维区间和)
int g[N],gt;
void up(int x){ //更新信息
sz[x]=sz[lc[x]]+sz[rc[x]]+1;
sum[x]=sum[lc[x]]+sum[rc[x]]+s[x].v;
L[x]=R[x]=s[x].x;
D[x]=U[x]=s[x].y;
if(lc[x]){
L[x]=min(L[x],L[lc[x]]);
R[x]=max(R[x],R[lc[x]]);
D[x]=min(D[x],D[lc[x]]);
U[x]=max(U[x],U[lc[x]]);
}
if(rc[x]){
L[x]=min(L[x],L[rc[x]]);
R[x]=max(R[x],R[rc[x]]);
D[x]=min(D[x],D[rc[x]]);
U[x]=max(U[x],U[rc[x]]);
}
}
int build(int l,int r){ //以序列g[l..r]为模板重建树,返回根节点
if(l>r)return 0;
int mid=(l+r)>>1;
lf ax=0,ay=0,sx=0,sy=0;
for(int i=l;i<=r;i++)ax+=s[g[i]].x,ay+=s[g[i]].y;
ax/=(r-l+1);
ay/=(r-l+1);
for(int i=l;i<=r;i++){
sx+=(ax-s[g[i]].x)*(ax-s[g[i]].x);
sy+=(ay-s[g[i]].y)*(ay-s[g[i]].y);
}
if(sx>sy)
nth_element(g+l,g+mid,g+r+1,cmp1),d[g[mid]]=1;
else
nth_element(g+l,g+mid,g+r+1,cmp2),d[g[mid]]=2;
lc[g[mid]]=build(l,mid-1);
rc[g[mid]]=build(mid+1,r);
up(g[mid]);
return g[mid];
}
void pia(int x){ //将树还原成序列g
if(!x)return;
pia(lc[x]);
g[++gt]=x;
pia(rc[x]);
}
void ins(int &x,int v){
if(!x){
x=v;
up(x);
return;
}
#define ch(f) (f?rc:lc)
if(d[x]==1)
ins(ch(s[v].x>s[x].x)[x],v);
else
ins(ch(s[v].y>s[x].y)[x],v);
up(x);
if(0.725*sz[x]<=max(sz[lc[x]],sz[rc[x]])){
gt=0;
pia(x);
x=build(1,gt);
}
}
void insert(int x,int y,int v){ //在(x,y)处插入元素
cur++;
s[cur]={x,y,v};
ins(rt,cur);
}
int x1,x2,y1,y2;
int qry(int x){
if(!x || x2<L[x] || x1>R[x] || y2<D[x] || y1>U[x])return 0;
if(x1<=L[x] && R[x]<=x2 && y1<=D[x] && U[x]<=y2)return sum[x];
int ret=0;
if(x1<=s[x].x && s[x].x<=x2 && y1<=s[x].y && s[x].y<=y2)
ret+=s[x].v;
return qry(lc[x])+qry(rc[x])+ret;
}
int query(int _x1,int _x2,int _y1,int _y2){ //查询[x1,x2]×[y1,y2]的区间和
x1=_x1; x2=_x2; y1=_y1; y2=_y2;
return qry(rt);
}
void init(){
rt=cur=0;
}
}tr;
莫队
- 离线(甚至在线)处理区间问题,
猛得一批
普通莫队
- 移动指针 \(l,r\) 来求所有区间的答案
- 块大小为 \(\sqrt n\),\(O(n^{\tfrac 3 2})\)
struct node{
int l,r,id;
bool operator<(const node &b)const{
if(l/unit!=b.l/unit)return l<b.l; //按块排序
if((l/unit)&1) //奇偶化排序
return r<b.r;
return r>b.r;
}
};
vector<node> query; //查询区间
int unit,n,bkt[N],a[N],final_ans[N]; //bkt是桶
ll ans;
void update(int x,int d){
int &b=bkt[a[x]];
ans-=C(b,2); //操作示例
b+=d;
ans+=C(b,2); //操作示例
}
void solve(){ //final_ans[]即最终答案
fill(bkt,bkt+n+1,0);
unit=int(ceil(sqrt(n)));
sort(query.begin(),query.end());
int l=1,r=0; ans=0; //如果原数组a编号从1开始
for(auto i:query){
while(l<i.l)update(l++,-1);
while(l>i.l)update(--l,1);
while(r<i.r)update(++r,1);
while(r>i.r)update(r--,-1);
final_ans[i.id]=ans;
}
}
//repeat(i,0,m)query.push_back({read(),read(),i}); //输入查询区间
带修莫队
- 相比与普通莫队,多了一个时间轴
- 块大小为 \(\sqrt[3]{nt}\),\(O(\sqrt[3]{n^4t})\)
- 空缺
二叉搜索树
- 左子树所有结点 \(\le v <\) 右子树所有节点,目前仅支持插入,查询可以写一个
map<int,TR *>
struct TR{
TR *ch[2],*fa; //ch[0]左儿子,ch[1]右儿子,fa父亲,根的父亲是inf
int v,dep; //v是结点索引,dep深度,根的深度是1
TR(TR *fa,int v,int dep):fa(fa),v(v),dep(dep){
mst(ch,0);
}
void insert(int v2){ //tr->insert(v2)插入结点
auto &c=ch[v2>v];
if(c==0)c=new TR(this,v2,dep+1);
else c->insert(v2);
}
}*tr=new TR(0,inf,0);
//inf是无效节点,用tr->ch[0]来访问根节点
一些建议
双头优先队列可以用multiset
支持插入、查询中位数可以用双堆
priority_queue<ll> h1; //大根堆
priority_queue< ll,vector<ll>,greater<ll> > h2; //小根堆
void insert(ll x){
#define maintain(h1,h2,b) {h1.push(x); if(h1.size()>h2.size()+b)h2.push(h1.top()),h1.pop();}
if(h1.empty() || h1.top()>x)maintain(h1,h2,1)
else maintain(h2,h1,0);
}
//h1.size()+h2.size()为奇数时h1.top()为中位数,偶数看题目定义
双关键字堆可以用两个multiset模拟
struct HEAP{
multiset<pii> a[2];
void init(){a[0].clear();a[1].clear();}
pii rev(pii x){return {x.second,x.first};}
void push(pii x){
a[0].insert(x);
a[1].insert(rev(x));
}
pii top(int p){
pii t=*--a[p].end();
return p?rev(t):t;
}
void pop(int p){
auto t=--a[p].end();
a[p^1].erase(a[p^1].lower_bound(rev(*t)));
a[p].erase(t);
}
};
高维前缀和
- 以二维为例,t是维数
- 法一 \(O(n^t2^t)\)
- 法二 \(O(n^tt)\)
//<1>
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
b[i][j]=b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j];
//<2>
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
a[i][j]+=a[i][j-1];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
a[i][j]+=a[i-1][j];
一个01串,支持把某位置的1改成0,查询某位置之后第一个1的位置,可以用并查集(删除 d[x]=d[x+1]
,查询 d[x]
)
图论
图论的一些概念
- 基环图:树加一条边
- 简单图:不含重边和自环
- 完全图:顶点两两相连的无向图
- 竞赛图:顶点两两相连的有向图
- 点u到v可达:有向图中,存在u到v的路径
- 点u和v联通:无向图中,存在u到v的路径
- 生成子图:点集和原图相同
- 导出子图/诱导子图:选取一个点集,尽可能多加边
- 正则图:所有点的度均相同的无向图
- 强正则图:与任意两个相邻的点相邻的点数相同,与任意两个不相邻的点相邻的点数相同的正则图
- 强正则图的点数 \(v\),度 \(k\),相邻的点的共度 \(\lambda\),不相邻的点的共度 \(\mu\) 有 \(k(k-1-\lambda)=\mu(v-1-k)\)
- 强正则图的例子:所有完全图、所有nk顶点满n分图
- 点割集:极小的,把图分成多个联通块的点集
- 割点:自身就是点割集的点
- 边割基:极小的,把图分成多个联通块的边集
- 桥:自身就是边割集的边
- 点联通度:最小点割集的大小
- 边联通度:最小边割集的大小
- Whitney定理:点联通度≤边联通度≤最小度
- 最大团:最大完全子图
- 最大独立集:最多的两两不连接的顶点
- 最小染色数:相邻的点不同色的最少色数
- 最小团覆盖数:覆盖整个图的最少团数
- 最大独立集即补图最大团
- 最小染色数等于补图最小团覆盖数
- 哈密顿通路:通过所有顶点有且仅有一次的路径,若存在则为半哈密顿图/哈密顿图
- 哈密顿回路:通过所有顶点有且仅有一次的回路,若存在则为哈密顿图
- 完全图 \(K_{2k+1}\) 的边集可以划分为 \(k\) 个哈密顿回路
- 完全图 \(K_{2k}\) 的边集去掉 \(k\) 条互不相邻的边后可以划分为 \(k-1\) 个哈密顿回路
图论基础
前向星
struct edge{int to,w,nxt;}; //指向,权值,下一条边
vector<edge> a;
int head[N];
void addedge(int x,int y,int w){
a.push_back((edge){y,w,head[x]});
head[x]=a.size()-1;
}
void init(int n){ //初始化
a.clear();
fill(head,head+n,-1);
}
//for(int i=head[x];i!=-1;i=a[i].nxt) //遍历x出发的边(x,a[i].to)
拓扑排序×Toposort
- \(O(V+E)\)
queue<int> q; int ideg[N]; vector<int> ans;
void toposort(){
repeat(x,0,n)for(auto p:a[x])ideg[p]++;
repeat(i,0,n)if(ideg[i]==0)q.push(i);
while(!q.empty()){
int x=q.front(); q.pop(); ans.push_back(x);
for(auto p:a[x])if(--ideg[p]==0)q.push(p);
}
}
欧拉路径 欧拉回路
- 若存在则路径为 \(dfs\) 退出序
- (不记录点的 \(vis\),只记录边的 \(vis\))
dfs树 bfs树
- 无向图dfs树:只有树边和返祖边,返祖边一定连接两个在根出发的全是树边的链上的顶点
- 有向图dfs树:树边、返祖边、横叉边、前向边
- 空缺
最短路径
Dijkstra
- 仅限正权,\(O(E\log E)\)
struct node{
int to; ll dis;
bool operator<(const node &b)const{
return dis>b.dis;
}
};
int n;
bool vis[N];
vector<node> a[N];
void dij(int s,ll dis[]){ //s是起点,dis是结果
fill(vis,vis+n+1,0);
fill(dis,dis+n+1,inf); dis[s]=0; //last[s]=-1;
static priority_queue<node> q; q.push({s,0});
while(!q.empty()){
int x=q.top().to; q.pop();
if(vis[x])continue; vis[x]=1;
for(auto i:a[x]){
int p=i.to;
if(dis[p]>dis[x]+i.dis){
dis[p]=dis[x]+i.dis;
q.push({p,dis[p]});
//last[p]=x; //last可以记录最短路(倒着)
}
}
}
}
Floyd
- \(O(V^3)\)
repeat(k,0,n)
repeat(i,0,n)
repeat(j,0,n)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
- 补充:
bitset
优化(只考虑是否可达),\(O(V^3)\)
//bitset<N> g<N>;
repeat(i,0,n)
repeat(j,0,n)
if(g[j][i])
g[j]|=g[i];
SPFA
- SPFA搜索中,有一个点入队 \(n+1\) 次即存在负环
- 编号从 \(0\) 开始,\(O(VE)\)
int cnt[N]; bool vis[N]; ll h[N]; //h意思和dis差不多,但是Johnson里需要区分
int n;
struct node{int to; ll dis;};
vector<node> a[N];
bool spfa(int s){ //返回是否有负环(s为起点)
repeat(i,0,n+1)
cnt[i]=vis[i]=0,h[i]=inf;
h[s]=0; //last[s]=-1;
static deque<int> q; q.assign(1,s);
while(!q.empty()){
int x=q.front(); q.pop_front();
vis[x]=0;
for(auto i:a[x]){
int p=i.to;
if(h[p]>h[x]+i.dis){
h[p]=h[x]+i.dis;
//last[p]=x; //last可以记录最短路(倒着)
if(vis[p])continue;
vis[p]=1;
q.push_back(p); //可以SLF优化
if(++cnt[p]>n)return 1;
}
}
}
return 0;
}
bool negcycle(){ //返回是否有负环
a[n].clear();
repeat(i,0,n)
a[n].push_back({i,0}); //加超级源点
return spfa(n);
}
Johnson
- SPFA+Dijkstra实现全源最短路,编号从 \(0\) 开始,\(O(VE\log E)\)
ll dis[N][N];
bool jn(){ //返回是否成功
if(negcycle())return 0;
repeat(x,0,n)
for(auto &i:a[x])
i.dis+=h[x]-h[i.to];
repeat(x,0,n)dij(x,dis[x]);
repeat(x,0,n)
repeat(p,0,n)
if(dis[x][p]!=inf)
dis[x][p]+=h[p]-h[x];
return 1;
}
最小环
- 有向图最小环Dijkstra,\(O(VE\log E)\):对每个点 \(v\) 进行Dijkstra,到达 \(v\) 的边更新答案,适用稀图
- 有向图最小环Floyd,\(O(V^3)\):Floyd完之后,任意两点计算 \(dis_{u,v}+dis_{v,u}\),适用稠图
- 无边权无向图最小环:以每个顶点为根生成bfs树(不是dfs),横叉边更新答案,\(O(VE)\)
- 有边权无向图最小环:上面的bfs改成Dijkstra,\(O(VE \log E)\)
//无边权无向图最小环
int dis[N],fa[N],n,ans;
vector<int> a[N];
queue<int> q;
void bfs(int s){ //求经过s的最小环(不一定是简单环)
fill(dis,dis+n,-1); dis[s]=0;
q.push(s); fa[s]=-1;
while(!q.empty()){
int x=q.front(); q.pop();
for(auto p:a[x])
if(p!=fa[x]){
if(dis[p]==-1){
dis[p]=dis[x]+1;
fa[p]=x;
q.push(p);
}
else ans=min(ans,dis[x]+dis[p]+1);
}
}
}
int mincycle(){
ans=inf;
repeat(i,0,n)bfs(i); //只要遍历最小环可能经过的点即可
return ans;
}
最小生成树×MST
Kruskal
- 对边长排序,然后添边,并查集判联通,\(O(E\log E)\),排序是瓶颈
DSU d;
struct edge{int u,v,dis;}e[200010];
ll kru(){
ll ans=0,cnt=0;
sort(e,e+m);
repeat(i,0,m){
int x=d[e[i].u],y=d[e[i].v];
if(x==y)continue;
d.join(x,y);
ans+=e[i].dis;
cnt++;
if(cnt==n-1)break;
}
if(cnt!=n-1)return -1;
else return ans;
}
Boruvka
- 类似Prim算法,但是可以多路增广(
名词迷惑行为),\(O(E\log V)\)
DSU d;
struct edge{int u,v,dis;}e[200010];
ll bor(){
ll ans=0;
d.init(n);
e[m].dis=inf;
vector<int> b; //记录每个联通块的增广路(名词迷惑行为)
bool f=1;
while(f){
b.assign(n,m);
repeat(i,0,m){
int x=d[e[i].u],y=d[e[i].v];
if(x==y)continue;
if(e[i].dis<e[b[x]].dis)
b[x]=i;
if(e[i].dis<e[b[y]].dis)
b[y]=i;
}
f=0;
for(auto i:b)
if(i!=m){
int x=d[e[i].u],y=d[e[i].v];
if(x==y)continue;
ans+=e[i].dis;
d.join(x,y);
f=1;
}
}
return ans;
}
最小树形图 | 朱刘算法
- 其实有更高级的Tarjan算法 \(O(E+V\log V)\),
但是学不会 - 编号从1开始,求的是叶向树形图,\(O(VE)\)
int n;
struct edge{int x,y,w;};
vector<edge> eset; //会在solve中被修改
ll solve(int rt){ //返回最小的边权和,返回-1表示没有树形图
static int fa[N],id[N],top[N],minw[N];
ll ans=0;
while(1){
int cnt=0;
repeat(i,1,n+1)
id[i]=top[i]=0,minw[i]=inf;
for(auto &i:eset) //记录权最小的父亲
if(i.x!=i.y && i.w<minw[i.y]){
fa[i.y]=i.x;
minw[i.y]=i.w;
}
minw[rt]=0;
repeat(i,1,n+1){ //标记所有环
if(minw[i]==inf)return -1;
ans+=minw[i];
for(int x=i;x!=rt && !id[x];x=fa[x])
if(top[x]==i){
id[x]=++cnt;
for(int y=fa[x];y!=x;y=fa[y])
id[y]=cnt;
break;
}
else top[x]=i;
}
if(cnt==0)return ans; //无环退出
repeat(i,1,n+1)
if(!id[i])
id[i]=++cnt;
for(auto &i:eset){ //缩点
i.w-=minw[i.y];
i.x=id[i.x],i.y=id[i.y];
}
n=cnt;
rt=id[rt];
}
}
树论的一些概念
树的直径
- 直径:即最长路径
- 求直径:以任意一点出发所能达到的最远节点为一个端点,以这个端点出发所能达到的最远节点为另一个端点(也可以树上dp)
树的重心
- 重心:以重心为根,其最大儿子子树最小
- 求重心:dfs计算所有子树大小,最后计算最大儿子子树最小的节点
- 性质
- 以重心为根,所有子树大小不超过整棵树的一半
- 重心最多有两个
- 重心到所有结点距离之和最小
- 两棵树通过一条边相连,则新树的重心在是原来两棵树重心的路径上
- 一棵树添加或删除一个叶子,重心最多移动一条边的距离
- 重心不一定在直径上
最近公共祖先
欧拉序列+st表解法
- 编号从 \(0\) 开始,初始化 \(O(n\log n)\),查询 \(O(1)\)
int n,m;
vector<int> a;
vector<int> e[500010];
bool vis[500010];
int pos[500010],dep[500010];
#define mininarr(a,x,y) (a[x]<a[y]?x:y)
struct RMQ{
#define logN 21
int f[N][logN],log[N];
RMQ(){
log[1]=0;
repeat(i,2,N)
log[i]=log[i/2]+1;
}
void build(){
int n=a.size();
repeat(i,0,n)
f[i][0]=a[i];
repeat(k,1,logN)
repeat(i,0,n-(1<<k)+1)
f[i][k]=mininarr(dep,f[i][k-1],f[i+(1<<(k-1))][k-1]);
}
int query(int l,int r){
if(l>r)swap(l,r);//!!
int s=log[r-l+1];
return mininarr(dep,f[l][s],f[r-(1<<s)+1][s]);
}
}rmq;
void dfs(int x,int d){
if(vis[x])return;
vis[x]=1;
dep[x]=d;
a.push_back(x);
pos[x]=a.size()-1; //记录位置
repeat(i,0,e[x].size()){
int p=e[x][i];
if(vis[p])continue;
dfs(p,d+1);
a.push_back(x);
}
}
int lca(int x,int y){
return rmq.query(pos[x],pos[y]);
}
//初始化:dfs(s,1); rmq.build();
树链剖分解法
- 编号从哪开始都可以,初始化 \(O(n)\),查询 \(O(\log n)\)
vector<int> e[N];
int dep[N],son[N],sz[N],top[N],fa[N]; //son重儿子,top链顶
void dfs1(int x){ //标注dep,sz,son,fa
sz[x]=1;
son[x]=-1;
dep[x]=dep[fa[x]]+1;
for(auto p:e[x]){
if(p==fa[x])continue;
fa[p]=x; dfs1(p);
sz[x]+=sz[p];
if(son[x]==-1 || sz[son[x]]<sz[p])
son[x]=p;
}
}
void dfs2(int x,int tv){ //标注top
top[x]=tv;
if(son[x]!=-1)dfs2(son[x],tv);
for(auto p:e[x]){
if(p==fa[x] || p==son[x])continue;
dfs2(p,p);
}
}
void init(int s){ //s是根
fa[s]=s;
dfs1(s);
dfs2(s,s);
}
int lca(int x,int y){
while(top[x]!=top[y])
if(dep[top[x]]>=dep[top[y]])x=fa[top[x]];
else y=fa[top[y]];
return dep[x]<dep[y]?x:y;
}
一些关于lca的问题
int length(int x,int y){ //路径长度
return dep[x]+dep[y]-2*dep[lca(x,y)];
}
联通性相关
强联通分量scc+缩点
- Tarjan,\(O(V+E)\)
stack<int> stk;
bool vis[N],instk[N];
int dfn[N],low[N],co[N],w[N]; //co:染色结果
vector<int> sz; //第i个分量的点数
int dcnt; //时间戳
void dfs(int x){ //Tarjan求强联通分量
if(vis[x])return; vis[x]=1;
stk.push(x); instk[x]=1;
dfn[x]=low[x]=++dcnt;
for(auto p:a[x]){
dfs(p);
if(instk[p])
low[x]=min(low[x],low[p]);
}
if(low[x]==dfn[x]){
int t; sz.push_back(0); //记录
do{
t=stk.top();
stk.pop();
instk[t]=0;
sz.back()+=w[t]; //记录
co[t]=sz.size()-1; //染色
}while(t!=x);
}
}
void shrink(){ //缩点
set<pii> eset;
fill(vis,vis+n,0);
//fill(instk,instk+n,0);
sz.clear();
eset.clear();
dcnt=0;
repeat(i,0,n)
if(!vis[i])
dfs(i);
repeat(i,0,n)
for(auto p:a[i])
if(co[i]!=co[p])
eset.insert({co[i],co[p]});
n=sz.size();
repeat(i,0,n){
a[i].clear();
w[i]=sz[i];
}
for(auto i:eset){
a[i.fi].push_back(i.se);
//a[i.se].push_back(i.fi);
}
}
割点
- Tarjan
bool vis[N],cut[N]; //cut即结果,cut[i]表示i是否为割点
int dfn[N],low[N];
int dcnt; //时间戳
void dfs(int x,bool isroot=1){
if(vis[x])return; vis[x]=1;
dfn[x]=low[x]=++dcnt;
int ch=0; cut[x]=0;
for(auto p:a[x]){
if(!vis[p]){
dfs(p,0);
low[x]=min(low[x],low[p]);
if(!isroot && low[p]>=dfn[x])
cut[x]=1;
ch++;
}
low[x]=min(low[x],dfn[p]);
}
if(isroot && ch>=2) //根节点判断方法
cut[x]=1;
}
2-sat问题
可行解
- 有 \(2n\) 个顶点,其中顶点 \(2i\) 和顶点 \(2i+1\) 中能且仅能选一个,边 \((u,v)\) 表示选了 \(u\) 就必须选 \(v\),求一个可行解
- 暴力版,可以跑出字典序最小的解,编号从 \(0\) 开始,\(O(VE)\),(
但是难以跑到上界)
struct twosat{ //暴力版
int n;
vector<int> g[N*2];
bool mark[N*2]; //mark即结果,表示是否选择了这个点
int s[N],c;
bool dfs(int x){
if(mark[x^1])return 0;
if(mark[x])return 1;
mark[s[c++]=x]=1;
for(auto p:g[x])
if(!dfs(p))
return 0;
return 1;
}
void init(int _n){
n=_n;
for(int i=0;i<n*2;i++){
g[i].clear();
mark[i]=0;
}
}
void add(int x,int y){ //这个函数随题意变化
g[x].push_back(y^1); //选了x就必须选y^1
g[y].push_back(x^1); //选了y就必须选x^1
}
bool solve(){ //返回是否存在解
for(int i=0;i<n*2;i+=2)
if(!mark[i] && !mark[i^1]){
c=0;
if(!dfs(i)){
while(c>0)mark[s[--c]]=0;
if(!dfs(i^1))return 0;
}
}
return 1;
}
}ts;
-
SCC缩点版,\(O(V+E)\),空缺
-
2-SAT计数
-
空缺(太恐怖了)
图上的NP问题
最大团+极大团计数
- 求最大团顶点数(和最大团),
g[][]
编号从 \(0\) 开始,\(O(\exp)\)
int g[N][N],f[N][N],v[N],Max[N],n,ans; //g[][]是邻接矩阵,n是顶点数
//vector<int> rec,maxrec; //maxrec是最大团
bool dfs(int x,int cur){
if(cur==0)
return x>ans;
repeat(i,0,cur){
int u=f[x][i],k=0;
if(Max[u]+x<=ans)return 0;
repeat(j,i+1,cur)
if(g[u][f[x][j]])
f[x+1][k++]=f[x][j];
//rec.push_back(u);
if(dfs(x+1,k))return 1;
//rec.pop_back();
}
return 0;
}
void solve(){
ans=0; //maxrec.clear();
repeat_back(i,0,n){
int k=0;
repeat(j,i+1,n)
if(g[i][j])
f[1][k++]=j;
//rec.clear(); rec.push_back(i);
if(dfs(1,k)){
ans++;
//maxrec=rec;
}
Max[i]=ans;
}
}
- 求极大团个数(和所有极大团),
g[][]
的编号从 \(1\) 开始!\(O(\exp)\)
int g[N][N],n;
//vector<int> rec; //存当前极大团
int ans,some[N][N],none[N][N]; //some是未搜索的点,none是废除的点
void dfs(int d,int sn,int nn){
if(sn==0 && nn==0)
ans++; //此时rec是其中一个极大图
//if(ans>1000)return; //题目要求_(:зゝ∠)_
int u=some[d][0];
for(int i=0;i<sn;++i){
int v=some[d][i];
if(g[u][v])continue;
int tsn=0,tnn=0;
for(int j=0;j<sn;++j)
if(g[v][some[d][j]])
some[d+1][tsn++]=some[d][j];
for(int j=0;j<nn;++j)
if(g[v][none[d][j]])
none[d+1][tnn++]=none[d][j];
//rec.push_back(v);
dfs(d+1,tsn,tnn);
//rec.pop_back();
some[d][i]=0;
none[d][nn++]=v;
}
}
void solve(){ //运行后ans即极大团数
ans=0;
for(int i=0;i<n;++i)
some[0][i]=i+1;
dfs(0,n,0);
}
最小染色数
- \(O(\exp)\),
n=17
可用
int n,m;
int g[N]; //二进制邻接矩阵
bool ind[1<<N]; //是否为(极大)独立集
int dis[1<<N];
vector<int> a; //存独立集
#define np (1<<n)
int bfs(){ //重复覆盖简略版
fill(dis,dis+np,inf); dis[0]=0;
auto q=queue<int>(); q.push(0);
while(!q.empty()){
int x=q.front(); q.pop();
for(auto i:a){
int p=x|i;
if(p==np-1)return dis[x]+1;
if(dis[p]>dis[x]+1){
dis[p]=dis[x]+1;
q.push(p);
}
}
}
return 0;
}
int solve(){ //返回最小染色数
mst(g,0);
for(auto i:eset){
int x=i.fi,y=i.se;
g[x]|=1<<y;
g[y]|=1<<x;
}
//求所有独立集
ind[0]=1;
repeat(i,1,np){
int w=63-__builtin_clzll(ll(i)); //最高位
if((g[w]&i)==0 && ind[i^(1<<w)])
ind[i]=1;
}
//删除所有不是极大独立集的独立集
repeat(i,1,np)
if(ind[i]){
for(int j=1;j<np;j<<=1)
if((i&j)==0 && ind[i|j]){
ind[i]=0;
break;
}
if(ind[i])
a.push_back(i); //记录极大独立集
}
return bfs();
}
弦图+区间图
- 弦是连接环上不相邻点的边;弦图是所有长度大于3的环都有弦的无向图(类似三角剖分)
- 单纯点:所有与v相连的点构成一个团,则v是一个单纯点
- 完美消除序列:即点集的一个排列 \([v_1,v_2,...,v_n]\) 满足任意 \(v_i\) 在 \([v_{i+1},...,v_n]\) 的导出子图中是一个单纯点
- 定理:无向图是弦图 \(\Leftrightarrow\) 无向图存在完美消除序列
- 定理:最大团顶点数 \(\le\) 最小染色数(弦图取等号)
- 定理:最大独立集顶点数 \(\le\) 最小团覆盖(弦图取等号)
- 最大势算法MCS求完美消除序列:每次求出与 \([v_{i+1},...,v_n]\) 相邻点数最大的点作为 \(v_i\)
e[][]
点编号从 \(1\) 开始!rec
下标从 \(1\) 开始!桶优化,\(O(V+E)\)
vector<int> e[N];
int n,rec[N]; //rec[1..n]是结果
int h[N],nxt[N],pre[N],vis[N],lab[N];
void del(int x){
int w=lab[x];
if(h[w]==x)h[w]=nxt[x];
pre[nxt[x]]=pre[x];
nxt[pre[x]]=nxt[x];
}
void mcs(){
fill(h,h+n+1,0);
fill(vis,vis+n+1,0);
fill(lab,lab+n+1,0);
iota(nxt,nxt+n+1,1);
iota(pre,pre+n+1,-1);
nxt[n]=0;
h[0]=1;
int w=0;
repeat_back(i,1,n+1){
int x=h[w];
rec[i]=x;
del(x);
vis[x]=1;
for(auto p:e[x])
if(!vis[p]){
del(p);
lab[p]++;
nxt[p]=h[lab[p]];
pre[h[lab[p]]]=p;
h[lab[p]]=p;
pre[p]=0;
}
w++;
while(h[w]==0)w--;
}
}
- 判断弦图(判断是否为完美消除序列):对所有 \(v_i\),\([v_{i+1},...,v_n]\) 中与 \(v_i\) 相连的最靠前一个点 \(v_j\) 是否与与 \(v_i\) 连接的其他点相连
- 编号规则同上,大佬:\(O(V+E)\),我:\(O((V+E)\log V)\)
bool judge(){ //返回是否是完美消除序列(先要跑一遍MCS)
static int s[N],rnk[N];
repeat(i,1,n+1){
rnk[rec[i]]=i;
sort(e[i].begin(),e[i].end()); //方便二分查找,内存足够直接unmap
}
repeat(i,1,n+1){
int top=0,x=rec[i];
for(auto p:e[x])
if(rnk[x]<rnk[p]){
s[++top]=p;
if(rnk[s[top]]<rnk[s[1]])
swap(s[1],s[top]);
}
repeat(j,2,top+1)
if(!binary_search(e[s[1]].begin(),e[s[1]].end(),s[j]))
return 0;
}
return 1;
}
- 其他弦图算法
int color(){ //返回最大团点数/最小染色数
return *max_element(lab+1,lab+n+1)+1;
/* //以下求最大团
static int rnk[N];
repeat(i,1,n+1)rnk[rec[i]]=i;
int x=max_element(lab+1,lab+n+1)-lab;
rec2.push_back(x);
for(auto p:e[x])
if(rnk[x]<rnk[p])
rec2.push_back(x);
*/
}
int maxindset(){ //返回最大独立集点数/最小团覆盖数
int ans=0;
fill(vis,vis+n+1,0);
repeat(i,1,n+1){
int x=rec[i];
if(!vis[x]){
ans++; //rec2.push_back(x); //记录最大独立集
for(auto p:e[x])
vis[p]=1;
}
}
return ans;
}
int cliquecnt(){ //返回极大团数
static int s[N],fst[N],rnk[N],cnt[N];
int ans=0;
repeat(i,1,n+1)rnk[rec[i]]=i;
repeat(i,1,n+1){
int top=0,x=rec[i];
for(auto p:e[x])
if(rnk[x]<rnk[p]){
s[++top]=p;
if(rnk[s[top]]<rnk[s[1]])
swap(s[1],s[top]);
}
fst[x]=s[1]; cnt[x]=top;
}
fill(vis,vis+n+1,0);
repeat(i,1,n+1){
int x=rec[i];
if(!vis[x])ans++;
if(cnt[x]>0 && cnt[x]>=cnt[fst[x]]+1)
vis[fst[x]]=1;
}
return ans;
}
- 区间图:给出的每个区间都看成点,有公共部分的两个区间之间连一条边
- 区间图是弦图(反过来不一定),可以应用弦图的所有算法
- 区间图的判定:所有弦图可以写成一个极大团树(所有极大团看成一个顶点,极大团之间有公共顶点就连一条边),区间图的极大团树是一个链
仙人掌 | 圆方树
- 仙人掌:每条边至多属于一个简单环的无向联通图
- 圆方树:原来的点称为圆点,每个环新建一个方点,环上的圆点都与方点连接
- 子仙人掌:以 \(r\) 为根,点 \(p\) 的子仙人掌是删掉 \(p\) 到 \(r\) 的所有简单路径后 \(p\) 所在的联通块。这个子仙人掌就是圆方树中以 \(r\) 为根时,\(p\) 子树中的所有圆点
二分图
二分图的一些概念
- 最小点覆盖 = 最大匹配
- 最大独立集 = 顶点数 - 最大匹配
- 最小路径覆盖 = (开点前)顶点数 - 最大匹配,右顶点未被匹配的都看作起点
- 最小带权点覆盖 = 点权之和 - 最大带权独立集(左式用最小割求)
- 霍尔定理:最大匹配 = 左顶点数 \(\Leftrightarrow\) 所有左顶点子集 \(S\) 都有 \(|S|\le|\omega(S)|\) ,\(\omega(S)\) 是 \(S\) 的领域
- 运用:若在最大匹配中有 \(t\) 个左顶点失配,因此最大匹配 = 左顶点数 - \(t\)
- 对任意左顶点子集 \(S\) 都有 \(|S|\le|\omega(S)|+t\),\(t\ge|S|-|\omega(S)|\) ,求右式最大值即可求最大匹配
最大匹配 | 匈牙利 or 网络流
- 匈牙利,\(O(VE)\)
int n,m; //n个左顶点,m个右顶点
vector<int> a[N]; //左顶点x与右顶点a[x][0..sz]有连接
int dcnt,mch[N],dfn[N]; //mch[p]表示与右顶点p连接的左顶点,dfn[p]==dcnt意味着右顶点p已访问
bool dfs(int x){
repeat(i,0,a[x].size()){
int p=a[x][i];
if(dfn[p]!=dcnt){
dfn[p]=dcnt;
if(mch[p]==-1 || dfs(mch[p])){
mch[p]=x;
return 1;
}
}
}
return 0;
}
int hungarian(){ //返回最大匹配数
int ans=0;
repeat(i,0,m)mch[i]=dfn[i]=-1; //初始化
repeat(i,0,n){
dcnt=i;
if(dfs(i))ans++;
}
return ans;
}
- 网络流建图跑Dinic,基于Dinic,\(O(VE^{\tfrac 1 2})\)
void ae(int x,int y){
addedge(x,y,1);
addedge(y,x,0);
}
cin>>n1>>n2>>m; //左顶点数,右顶点数,之间的边数
n=n1+n2+2; //网络顶点数
s=0,t=n1+n2+1; //源点和汇点
init();
repeat(i,1,n1+1)ae(s,i); //构造源点与左顶点的边
repeat(i,n1+1,n1+n2+1)ae(i,t); //构造右顶点与汇点的边
repeat(i,0,m){
int x,y; cin>>x>>y; x--,y--;
ae(x+1,n1+y+1); //构造左顶点与右顶点的边
}
cout<<dinic()<<endl;
最大权匹配 | KM
- 求满二分图的最大权匹配
- 如果没有边就建零边,而且要求n<=m
- \(O(n^3)\)
int e[N][N],n,m; //邻接矩阵,左顶点数,右顶点数
int lx[N],ly[N]; //顶标
int mch[N]; //右顶点i连接的左顶点编号
bool fx[N],fy[N]; //是否在增广路上
bool dfs(int i){
fx[i]=1;
repeat(j,0,n)
if(lx[i]+ly[j]==e[i][j] && !fy[j]){
fy[j]=1;
if(mch[j]==-1 || dfs(mch[j])){
mch[j]=i;
return 1;
}
}
return 0;
}
void update(){
int fl=inf;
repeat(i,0,n)if(fx[i])
repeat(j,0,m)if(!fy[j])
fl=min(fl,lx[i]+ly[j]-e[i][j]);
repeat(i,0,n)if(fx[i])lx[i]-=fl;
repeat(j,0,m)if(fy[j])ly[j]+=fl;
}
int solve(){ //返回匹配数
repeat(i,0,n){
mch[i]=-1;
lx[i]=ly[i]=0;
repeat(j,0,m)
lx[i]=max(lx[i],e[i][j]);
}
repeat(i,0,n)
while(1){
repeat(j,0,m)
fx[j]=fy[j]=0;
if(dfs(i))break;
else update();
}
int ans=0;
repeat(i,0,m)
if(mch[i]!=-1)
ans+=e[mch[i]][i];
return ans;
}
稳定婚姻 | 延迟认可
- 稳定意味着不存在一对不是情侣的男女,都认为当前伴侣不如对方
- 编号从 \(0\) 开始,\(O(n^2)\)
struct node{
int s[N]; //s的值给定
//对男生来说是女生编号排序
//对女生来说是男生的分数
int now; //选择的伴侣编号
}a[N],b[N]; //男生,女生
int tr[N]; //男生尝试表白了几次
queue<int> q; //单身狗(男)排队
bool match(int x,int y){ //配对,返回是否成功
int x0=b[y].now;
if(x0!=-1){
if(b[y].s[x]<b[y].s[x0])
return 0; //分数不够,竞争失败
q.push(x0);
}
a[x].now=y;
b[y].now=x;
return 1;
}
void stable_marriage(){ //运行后a[].now,b[].now即结果
q=queue<int>();
repeat(i,0,n){
b[i].now=-1;
q.push(i);
tr[i]=0;
}
while(!q.empty()){
int x=q.front(); q.pop();
int y=a[x].s[tr[x]++]; //下一个最中意女生
if(!match(x,y))
q.push(x); //下次努力
}
}
网络流
网络流的一些概念
- \(c(u,v)\) 为 \(u\) 到 \(v\) 的容量,\(f(u,v)\) 为 \(u\) 到 \(v\) 的流量,\(f(u,v)<c(u,v)\)
- \(c[X,Y]\) 为 \(X\) 到 \(Y\) 的容量和,不包括 \(Y\) 到 \(X\) 的容量;\(f(X,Y)\) 为 \(X\) 到 \(Y\) 的流量和,要减去 \(Y\) 到 \(X\) 的流量
- 费用流(最小费用最大流):保证最大流后的最小费用
- 割:割 \([S,T]\) 是点集的一个分割且 \(S\) 包含源点,\(T\) 包含汇点,称 \(f(S,T)\) 为割的净流,\(c[S,T]\) 为割的容量
- 最大流最小割定理:最大流即最小割容量
- 求最小割:在最大流残量网络中,令源点可达的点集为 \(S\),其余的为 \(T\) 即可(但是满流边不一定都在 \(S,T\) 之间)
最大流 | Dinic or ISAP
-
以下顶点编号均从 \(0\) 开始
-
Dinic,多路增广,\(O(V^2E)\),然而并没有封装起来,
ISAP不香吗
struct edge{int to,w,nxt;}; //指向,限流,下一条边
vector<edge> a;
int head[N],cur[N];
void addedge(int x,int y,int w){
a.push_back((edge){y,w,head[x]});
head[x]=a.size()-1;
}
int n,m,s,t; //点数,边数,源点,汇点
queue<int> q;
bool inque[N]; //在队里的不需要入队
int dep[N]; //深度
bool bfs(){ //记录深度
fill(dep,dep+n,inf); dep[s]=0;
copy(head,head+n,cur); //当前弧初始化
q=queue<int>(); q.push(s);
while(!q.empty()){
int x=q.front(); q.pop();
inque[x]=0;
for(int i=head[x];i!=-1;i=a[i].nxt){
int p=a[i].to;
if(dep[p]>dep[x]+1 && a[i].w){
dep[p]=dep[x]+1;
if(inque[p]==0){
inque[p]=1;
q.push(p);
}
}
}
}
return dep[t]!=inf;
}
int dfs(int x,int flow){
int now,ans=0;
if(x==t)return flow;
for(int i=cur[x];i!=-1;i=a[i].nxt){ //当前弧开始(可以不重复访问废边)
cur[x]=i; //记录当前弧
int p=a[i].to;
if(a[i].w && dep[p]==dep[x]+1)
if((now=dfs(p,min(flow,a[i].w)))){
a[i].w-=now;
a[i^1].w+=now;
ans+=now,flow-=now; //流量更新
if(flow==0)break;
}
}
return ans;
}
void init(int n){ //初始化
a.clear();
fill(head,head+n,-1);
fill(inque,inque+n,0);
}
int solve(){ //返回最大流
int ans=0;
while(bfs())
ans+=dfs(s,inf);
return ans;
}
#define add(x,y,w) addedge(x,y,w),addedge(y,x,0)
//赋值n,再init(n),再添边和s,t赋值,最后solve()
- ISAP,仅一次bfs与多路增广,\(O(V^2E)\)
struct FLOW{ //ISAP最大流
struct edge{int to,w,nxt;}; //指向,限流,下一条边
vector<edge> a; int head[N]; //前向星
int cur[N]; //当前弧
int n,s,t; //点数,源点,汇点
queue<int> q;
int dep[N],gap[N]; //gap[x]为等于x的dep[i]的个数
void ae(int x,int y,int w){
a.push_back((edge){y,w,head[x]});
head[x]=a.size()-1;
}
bool bfs(){ //记录dep和gap
fill(dep,dep+n,-1); dep[t]=0;
fill(gap,gap+n,0); gap[0]=1;
q.push(t);
while(!q.empty()){
int x=q.front(); q.pop();
for(int i=head[x];i!=-1;i=a[i].nxt){
int p=a[i].to;
if(dep[p]!=-1)continue;
dep[p]=dep[x]+1;
q.push(p);
gap[dep[p]]++;
}
}
return dep[s]!=-1;
}
int dfs(int x,int fl){ //多路增广
int now,ans=0;
if(x==t)return fl;
for(int i=cur[x];i!=-1;i=a[i].nxt){ //当前弧开始(可以不重复访问废边)
cur[x]=i; //记录当前弧
int p=a[i].to;
if(a[i].w && dep[p]+1==dep[x])
if((now=dfs(p,min(fl,a[i].w)))){
a[i].w-=now;
a[i^1].w+=now;
ans+=now,fl-=now; //流量更新
if(fl==0)return ans;
}
}
gap[dep[x]]--;
if(gap[dep[x]]==0)dep[s]=n;
dep[x]++;
gap[dep[x]]++;
return ans;
}
void init(int _n){ //初始化
n=_n+1;
a.clear();
fill(head,head+n,-1);
}
int solve(int _s,int _t){ //返回最大流
s=_s,t=_t;
int ans=0;
if(bfs())
while(dep[s]<n){
copy(head,head+n,cur); //当前弧初始化
ans+=dfs(s,inf);
}
return ans;
}
}flow;
#define add(x,y,w) flow.ae(x,y,w),flow.ae(y,x,0)
//先flow.init(n),再add添边,最后flow.solve(s,t)
最小费用最大流 | MCMF
-
费用流一般指最小费用最大流(最大费用最大流把费用取反即可)
-
MCMF,单路增广,\(O(VE^2)\)(其实还有类Dinic算法,
有空再补)
struct FLOW{ //MCMF费用流
struct edge{int to,w,cost,nxt;}; //指向,限流,费用,下一条边
vector<edge> a; int head[N]; //前向星
int n,s,t,totcost; //点数,源点,汇点,总费用
deque<int> q;
bool inque[N]; //在队里的不需要入队
int dis[N]; //费用
struct{int to,e;}pre[N]; //路径的前一个点,这条边的位置
void ae(int x,int y,int w,int cost){
a.push_back((edge){y,w,cost,head[x]});
head[x]=a.size()-1;
}
bool spfa(){ //已死的算法
fill(dis,dis+n,inf); dis[s]=0;
q.assign(1,s);
while(!q.empty()){
int x=q.front(); q.pop_front();
inque[x]=0;
for(int i=head[x];i!=-1;i=a[i].nxt){
int p=a[i].to;
if(dis[p]>dis[x]+a[i].cost && a[i].w){
dis[p]=dis[x]+a[i].cost;
pre[p]={x,i};
if(inque[p]==0){
inque[p]=1;
if(!q.empty()
&& dis[q.front()]<=dis[p])
q.push_back(p);
else q.push_front(p);
//松弛,或者直接q.push_back(p);
}
}
}
}
return dis[t]!=inf;
}
void init(int _n){ //初始化
n=_n+1;
a.clear();
fill(head,head+n,-1);
fill(inque,inque+n,0);
}
int solve(int _s,int _t){ //返回最大流,费用存totcost里
s=_s,t=_t;
int ans=0;
totcost=0;
while(spfa()){
int fl=inf;
for(int i=t;i!=s;i=pre[i].to)
fl=min(fl,a[pre[i].e].w);
for(int i=t;i!=s;i=pre[i].to){
a[pre[i].e].w-=fl;
a[pre[i].e^1].w+=fl;
}
totcost+=dis[t]*fl;
ans+=fl;
}
return ans;
}
}flow;
#define add(x,y,w,cost) flow.ae(x,y,w,cost),flow.ae(y,x,0,-cost)
//先flow.init(n),再add添边,最后flow.solve(s,t)
图论杂项
矩阵树定理
无向图矩阵树定理
- 最小生成树计数
void matrix::addedge(int x,int y){
a[x][y]--,a[y][x]--;
a[x][x]++,a[y][y]++;
}
lf matrix::treecount(){
//for(auto i:eset)addedge(i.fi,i.se); //加边
n--,m=n; //a[n-1][n-1]的余子式(选任一节点均可)
return get_det();
}
有向图矩阵树定理
- 根向树形图计数,每条边指向父亲
- (叶向树形图,即每条边指向儿子,只要修改一个地方)
- 如果要求所有根的树形图之和,就求逆的主对角线之和乘以行列式(\(A^*=|A|A^{-1}\))
void matrix::addedge(int x,int y){
a[x][y]--;
a[x][x]++; //叶向树形图改成a[y][y]++;
}
ll matrix::treecount(){
//for(auto i:eset)addedge(i.fi,i.se); //加边
repeat(i,s,n) //s是根节点
repeat(j,0,n)
a[i][j]=a[i+1][j];
repeat(i,0,n)
repeat(j,s,n)
a[i][j]=a[i][j+1];
n--,m=n; //a[s][s]的余子式
return get_det();
}
BSET定理
- 有向欧拉图的欧拉回路总数等于任意根的根向树形图个数乘以 \(\Pi(deg(v)-1)!\)(←阶乘)(\(deg(v)\) 是 \(v\) 的入度或出度,
反正入度等于出度)
Prufer序列
- \(n\) 个点的无根树与长度 \(n-2\) 值域 \([1,n]\) 的序列有双射关系,Prufer序列就是其中一种
- 无根树转Prufer:设无根树点数为 \(n\),每次删除编号最小的叶子并记录它所连接的点的编号,进行 \(n-2\) 次操作
- Prufer转无根树:计算每个点的度为在序列中出现的次数加 \(1\),每次找度为 \(1\) 的编号最小的点与序列中第一个点连接,并将后者的度减 \(1\)
- Cayley定理:完全图 \(K_n\) 有 \(n^{n-2}\) 棵生成树
- 扩展:\(k\) 个联通块,第 \(i\) 个联通块有 \(s_i\)个点,则添加 \(k-1\) 条边使整个图联通的方案数有 \(n^{k-2}\Pi_{i=1}^k s_i\) 个
LGV引理
- DAG上固定 \(2n\) 个点 \([A_1,\cdots,A_n,B_1,\cdots,B_n]\),若有 \(n\) 条路径 \([A_1→B_1,\cdots,A_n→B_n]\) 两两不相交,则方案数为
- \(M=\left|\begin{array}{c}e(A_1,B_1)&\cdots &e(A_1,B_n)\\\vdots&\ddots&\vdots\\e(A_n,B_1)&\cdots&e(A_n,B_n)\end{array}\right|\)
- 其中 \(e(u,v)\) 表示 \(u→v\) 的路径计数
others of 图论杂项
Havel-Hakimi定理
- 给定一个度序列,反向构造出这个图
- 解:贪心,每次让剩余度最大的顶点 \(k\) 连接其余顶点中剩余度最大的 \(deg_k\) 个顶点
- (我认为二路归并比较快,可是找到的代码都用了
sort()
)
无向图三元环计数
- 无向图定向,\(pii(deg_i,i)>pii(deg_j,j)\Leftrightarrow\) 建立有向边 \((i,j)\)。然后暴力枚举 \(u\),将 \(u\) 的所有儿子 \(\omega(u)\) 标记为 \(dcnt\),暴力枚举 \(v∈\omega(u)\),若 \(v\) 的儿子被标记为 \(dcnt\) 则 \(ans++\),\(O(E\log E)\)
字符串
- (
我字符串是最菜的) - 寻找模式串p在文本串t中的所有出现
字符串hash
- 如果不需要区间信息,可以调用
hash<string>()(s)
获得ull范围的hash值 - 碰撞概率:单哈希 \(10^6\) 次比较大约有 \(\dfrac 1 {1000}\) 概率碰撞
- 支持查询子串hash值,初始化 \(O(n)\),子串查询 \(O(1)\)
const int hashxor=rnd()%1000000000; //如果不是cf可以不用hashxor
struct Hash{
vector<ll> a[2],p[2];
const ll b=257,m[2]={1000000007,998244353};
Hash(){repeat(i,0,2)a[i]={0},p[i]={1};}
void push(const string &s){
repeat(i,0,2)
for(auto c:s){
a[i]+=(a[i].back()*b+(c^hashxor))%m[i];
p[i]+=p[i].back()*b%m[i];
}
}
pair<ll,ll> get(int l,int r){
#define q(i) (a[i][r+1]-a[i][l]*p[i][r-l+1]%m[i]+m[i])%m[i]
return {q(0),q(1)};
}
int size(){return a[0].size()-1;}
pair<ll,ll> prefix(int len){return get(0,len-1);}
pair<ll,ll> suffix(int len){return get(size()-len,size()-1);}
}h;
字符串函数
前缀函数×kmp
- \(p[x]\) 表示满足
s.substr(0,k)==s.substr(x-k,k)
且 \(x\not=k\) 的 \(k\) 的最大值,\(p[0]=0\) - 线性复杂度
int p[N];
void kmp(const string &s){ //求s的前缀函数
p[0]=0; int k=0;
repeat(i,1,s.length()){
while(k>0 && s[i]!=s[k])k=p[k-1];
if(s[i]==s[k])k++;
p[i]=k;
}
}
void solve(string s1,string s2){ //模拟s1.find(s2)
kmp(s2+'#'+s1);
repeat(i,s2.size()+1,s.size())
if(p[i]==(int)s2.size())
ans.push_back(i-2*s2.size()); //编号从0开始的左端点
}
struct KMP{ //kmp自动机
string s; int k;
vector<int> p;
int get(char c){
while(k>0 && c!=s[k])k=p[k-1];
if(c==s[k])k++;
return k;
}
KMP(const string &_s){
p.push_back(k=0);
s=_s+'#'; repeat(i,1,s.size())p+=get(s[i]);
}
int size(){return s.size()-1;}
};
void solve(string s1,string s2){ //模拟s1.find(s2)
KMP kmp(s2);
repeat(i,0,s1.size())
if(kmp.get(s1[i])==kmp.size())
ans.push_back(i+1-kmp.size()); //编号从0开始的左端点
kmp.k=0; //清空(如果下次还要用的话)
}
z函数×exkmp
- \(z[x]\) 表示满足
s.substr(0,k)==s.substr(x,k)
的 \(k\) 的最大值,\(z[0]=0\) - 线性复杂度
int z[N];
void exkmp(const string &s){ //求s的z函数
fill(z,z+s.size(),0); int l=0,r=0;
repeat(i,1,s.size()){
if(i<=r)z[i]=min(r-i+1,z[i-l]);
while(i+z[i]<(int)s.size() && s[z[i]]==s[i+z[i]])z[i]++;
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}
}
马拉车×Manacher
- 预处理为
"#*A*A*A*A*A*"
- 线性复杂度
int len[N*2]; char s[N*2]; //两倍内存
int manacher(char s1[]){ //s1可以是s
int n=strlen(s1)*2+1;
repeat_back(i,0,n)s[i+1]=(i%2==0?'*':s1[i/2]);
n++; s[0]='#'; s[n++]=0;
len[0]=0;
int mx=0,id=0,ans=0;
repeat(i,1,n-1){
if(i<mx)len[i]=min(mx-i,len[2*id-i]);
else len[i]=1;
while(s[i-len[i]]==s[i+len[i]])len[i]++;
if(len[i]+i>mx)mx=len[i]+i,id=i;
ans=max(ans,len[i]-1); //最长回文串长度
}
return ans;
}
最小表示法
- 求 \(s\) 重复无数次的字符串最小后缀的左端点
- 线性复杂度
int minstr(const string &s){
int k=0,i=0,j=1,n=s.size();
while(max(k,max(i,j))<n){
if(s[(i+k)%n]==s[(j+k)%n])k++;
else{
s[(i+k)%n]>s[(j+k)%n]?i+=k+1:j+=k+1;
if(i==j)i++;
k=0;
}
}
return min(i,j);
}
后缀数组×SA
- \(sa[i]\) 表示所有后缀中第 \(i\) 小的后缀是
s.substr(sa[i],-1)
- \(rk[i]\) 表示所有后缀中
s.substr(i,-1)
是第 \(rk[i]\) 小 - 编号从 \(1\) 开始!\(O(n\log n)\)
int sa[N],rk[N]; //sa,rk即结果
void get_sa(const string &S){
static int pre[N*2],id[N],px[N],cnt[N];
int n=S.length(),m=256;
const char *const s=S.c_str()-1; //为了编号从1开始
for(int i=1;i<=n;i++)cnt[rk[i]=s[i]]++;
for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[rk[i]]--]=i;
for(int w=1;w<n;w<<=1){
int t=0;
for(int i=n;i>n-w;i--)id[++t]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)id[++t]=sa[i]-w;
mst(cnt,0);
for(int i=1;i<=n;i++)cnt[px[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[px[i]]--]=id[i];
memcpy(pre,rk,sizeof(rk));
int p=0;
static auto pp=[&](int x){return pii(pre[x],pre[x+w]);};
for(int i=1;i<=n;i++)
rk[sa[i]]=pp(sa[i])==pp(sa[i-1])?p:++p;
m=p; //优化计数排序值域
}
}
- sa可以在同一文本串中在线多次查找模式串(二分查找)
height数组
- 定义 \(lcp(i,j)=\) 后缀 \(i\) 和后缀 \(j\) 的最长公共前缀长度
- 定义 \(height[i]=lcp(sa[i],sa[i-1])\),\(height[1]=0\)
- \(height[rk[i]]\ge height[rk[i-1]]-1\)
- 编号从 \(1\) 开始,\(O(n)\)
for(int i=1,k=0;i<=n;i++){
if(k)k--;
while(s[i+k]==s[sa[rk[i]-1]+k])k++;
ht[rk[i]]=k;
}
- 不相等的子串个数为 \(\dfrac{n(n+1)}{2}-\sum\limits_{i=2}^{n}height[i]\)
自动机
字典树×Trie
- 线性复杂度
struct trie{
int a[N][26],cnt[N],t;
void init(){
t=0; add();
}
int add(){
mst(a[t],0);
cnt[t]=0;
return t++;
}
void insert(const char s[]){
int k=0;
for(int i=0;s[i];i++){
int c=s[i]-'a'; //小写字母
if(!a[k][c])a[k][c]=add();
k=a[k][c];
//son[k]++; //如果要记录子树大小
}
cnt[k]++;
}
int query(const char s[]){
int k=0;
for(int i=0;s[i];i++){
int c=s[i]-'a'; //小写字母
if(!a[k][c])return 0;
k=a[k][c];
}
return cnt[k];
}
}t;
AC自动机
- 先构建字典树,再构建fail树和字典图
- 线性复杂度
struct AC{
static const int sigma=26;
struct node{
int to[sigma],fail,trie_cnt,cnt;
int &operator[](int x){return to[x];}
//vector<int> p; //指向模式串集合
}a[N];
int t;
void init(){t=0; a[t++]=node(); q.clear();}
void insert(const char s[],int p){ //将模式串插入字典树
int k=0;
for(int i=0;s[i];i++){
int c=s[i]-'a'; //小写字母
if(!a[k][c])a[k][c]=t,a[t++]=node();
k=a[k][c];
}
a[k].trie_cnt++;
//a[k].p+=p;
}
vector<int> q; //存了bfs序
void build(){ //构建fail树,将字典树扩展为图
int tail=0;
repeat(i,0,sigma)
if(a[0][i])
q+=(a[0][i]);
while(tail!=(int)q.size()){
int k=q[tail++];
repeat(i,0,sigma)
if(a[k][i])
a[a[k][i]].fail=a[a[k].fail][i],q+=(a[k][i]);
else
a[k][i]=a[a[k].fail][i];
}
}
void query(const char s[]){ //记录文本串中模式串出现次数
int k=0;
for(int i=0;s[i];i++){
int c=s[i]-'a'; //小写字母
k=a[k][c];
a[k].cnt++; //fail树上差分
}
repeat_back(i,0,q.size()){ //反向遍历bfs序
a[a[q[i]].fail].cnt+=a[q[i]].cnt; //差分求和
//for(auto p:a[q[i]].p)ans[p]=a[q[i]].cnt; //反馈答案
}
}
}tr;
杂项
位运算
位运算巨佬操作
- 中点向下取整
(x+y)/2: (x & y) + ((x ^ y) >> 1)
- 中点向上取整
(x+y+1)/2: (x | y) - ((x ^ y) >> 1)
- 一般来说用
x + (y - x >> 1)
- 一般来说用
abs(n): (n ^ (n >> 31)) - (n >> 31)
max(a,b): b & ((a - b) >> 31) | a & (~(a - b) >> 31)
min(a,b): a & ((a - b) >> 31) | b & (~(a - b) >> 31)
位运算函数
- (不需要头文件)
__builtin_ctz(x),__builtin_ctzll(x) //返回x后导0的个数,x是0则返回32 or 64
__builtin_clz(x),__builtin_clzll(x) //返回x前导0的个数,x是0则返回32 or 64
__builtin_popcount(x),__builtin_popcountll(x) //返回x中1的个数
__builtin_parity(x),__builtin_parityll(x) //返回x中1的个数是否为奇数
枚举二进制子集
- 枚举二进制数m的非空子集
for(int s=m;s;s=(s-1)&m){
work(s);
}
- 枚举n个元素的大小为k的二进制子集(要保证k不等于0)
int s=(1<<k)-1;
while(s<(1<<n)){
work(s);
int x=s&-s,y=s+x;
s=((s&~y)/x>>1)|y; //这里有一个位反~
}
浮点数
浮点数误差分析
const lf err=1e-11;
if(fabs(x)<err)x=fabs(x); //输出浮点数的预处理
浮点数常量
float 1e38, 有效数字6
double 1e308, 有效数字15
long double 1e4932, 有效数字18
常数优化
估计函数用时
clock()
可以获取时刻,单位毫秒,运行函数前后的时间之差即为用时- 一些巨佬测出来的结论:
- 整数加减:1(个时间单位,下同)
- 整数位运算:1
- 整数乘法:2
- 整数除法:21
- 浮点加减:3
- 浮点除法:35
- 浮点开根:60
快读快写
ll read(){
ll x=0,tag=1; char c=getchar();
for(;!isdigit(c);c=getchar())if(c=='-')tag=-1;
for(; isdigit(c);c=getchar())x=x*10+c-48;
return x*tag;
}
void write(ll x){ //可能不比printf快
if(x<0)x=-x,putchar('-');
if(x>=10)write(x/10);
putchar(x%10^48);
}
char getc(){ //代替getchar,用了这个就不能用其他读入函数如scanf
static char now[1<<16],*S,*T;
if(T==S){T=(S=now)+fread(now,1,1<<16,stdin); if(T==S)return EOF;}
return *S++;
}
STL手写内存分配器
static char space[10000000],*sp=space;
template<typename T>
struct allc:allocator<T>{
allc(){}
template<typename U>
allc(const allc<U> &a){}
template<typename U>
allc<T>& operator=(const allc<U> &a){return *this;}
template<typename U>
struct rebind{typedef allc<U> other;};
T* allocate(size_t n){
T *res=(T*)sp;
sp+=n*sizeof(T);
return res;
}
void deallocate(T* p,size_t n){}
};
vector< int,allc<int> > a;
其他优化
//(都是听说的)
1ll*a 比 (ll)a 快
取模:x%mod 优化为 x<mod?x:x%mod
减少浮点除法:a/b+c/d 优化为 (a*d+b*c)/(b*d)
精度足够时用ll代替浮点类型
三目运算符替代if语句
多路并行运算,如 (a+b)+(c+d) 比 ((a+b)+c)+d 快
加上register和inline,以及强制内联__inline __attribute__((always_inline))
多重for循环时,修改for的顺序保证内存连续访问
多使用局部变量
在TLE边缘试探
while(clock()<0.9*CLOCKS_PER_SEC){
//反复更新最优解
}
对拍
#include <stdio.h>
#include <stdlib.h>
int main(){
//For Windows
//对拍时不开文件输入输出
//当然,这段程序也可以改写成批处理的形式
while(1){
system("gen > test.in"); //数据生成器将生成数据写入输入文件
system("test1.exe < test.in > a.out"); //获取程序1输出
system("test2.exe < test.in > b.out"); //获取程序2输出
if(system("fc a.out b.out")){
//该行语句比对输入输出
//fc返回0时表示输出一致,否则表示有不同处
system("pause"); //方便查看不同处
return 0;
//该输入数据已经存放在test.in文件中,可以直接利用进行调试
}
}
}
战术分析
(我真的真的真的太南了)
- 可能造成WA的注意点
ll t; 1<<t返回int,必须是1ll<<t
int x; x<<y的y会先对32取模
operator<的比较内容一定要写完整
试一试输入^Z能否结束
无向图输入要给两个值赋值g[x][y]=g[x][y]=1
多组输入时,图记得初始化
建模的转换函数的宏定义一定要加括号,或者写成函数
- 可能造成TLE的注意点
图论用forward_list吧,贼好用,deque可以用list替代(近期研究发现,list等要手写内存池才能巨快qwq)
- 可能连代码都写不出的注意点
多用相空间角度思考问题
内存比我想象的要大一些(有时候1e7可以塞下)
在64位编译器(我的编译器)中set每个元素需要额外32字节内存
数学
数论
基本操作
模乘 模幂 模逆 扩欧
ll mul(ll a,ll b,ll m=mod){return a*b%m;} //模乘
ll qpow(ll a,ll b,ll m=mod){ //快速幂
ll ans=1;
for(;b;a=mul(a,a,m),b>>=1)
if(b&1)ans=mul(ans,a,m);
return ans;
}
void exgcd(ll a,ll b,ll &d,ll &x,ll &y){ //扩欧,ax+by=gcd(a,b),d存gcd
if(!b)d=a,x=1,y=0;
else exgcd(b,a%b,d,y,x),y-=x*(a/b);
}
ll gcdinv(ll v,ll m=mod){ //扩欧版逆元
ll d,x,y;
exgcd(v,m,d,x,y);
return (x%m+m)%m;
}
ll getinv(ll v,ll m=mod){ //快速幂版逆元,m必须是质数!!
return qpow(v,m-2,m);
}
ll qpows(ll a,ll b,ll m=mod){
if(b>=0)return qpow(a,b,m);
else return getinv(qpow(a,-b,m),m);
}
防爆模乘
//int128版本
ll mul(ll a,ll b,ll m=mod){return (__int128)a*b%m;}
//llf版本(欲防爆,先自爆)(注意在测试的时候不知道为什么有锅)
ll mul(ll a,ll b,ll m=mod){return (a*b-ll((llf)a/m*b)*m+m)%m;}
//每位运算一次版本,注意这是真·龟速乘,O(logn)
ll mul(ll a,ll b,ll m=mod){
ll ans=0;
while(b){
if(b&1)ans=(ans+a)%m;
a=(a+a)%m;
b>>=1;
}
return ans;
}
//把b分成两部分版本,要保证m小于1<<42(约等于4e12),a,b<m
ll mul(ll a,ll b,ll m=mod){
a%=m,b%=m;
ll l=a*(b>>21)%m*(1ll<<21)%m;
ll r=a*(b&(1ll<<21)-1)%m;
return (l+r)%m;
}
最大公约数
__gcd(a,b) //内置gcd,推荐
ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);} //不推荐233,比内置gcd慢
ll gcd(ll a,ll b){ //卡常gcd来了!!
#define tz __builtin_ctzll
if(!a || !b)return a|b;
int t=tz(a|b);
a>>=tz(a);
while(b){
b>>=tz(b);
if(a>b)swap(a,b);
b-=a;
}
return a<<t;
#undef tz
}
- 实数gcd
lf fgcd(lf a,lf b){return fabs(b)<1e-5?a:fgcd(b,fmod(a,b));}
高级模操作
同余方程组 | CRT+extra
//CRT,m[i]两两互质
ll crt(ll a[],ll m[],int n){ //ans%m[i]==a[i]
repeat(i,0,n)a[i]%=m[i];
ll M=1,ans=0;
repeat(i,0,n)
M*=m[i];
repeat(i,0,n){
ll k=M/m[i],t=gcdinv(k%m[i],m[i]); //扩欧!!
ans=(ans+a[i]*k*t)%M; //两个乘号可能都要mul
}
return (ans+M)%M;
}
//exCRT,m[i]不需要两两互质,基于扩欧exgcd和龟速乘mul
ll excrt(ll a[],ll m[],int n){ //ans%m[i]==a[i]
repeat(i,0,n)a[i]%=m[i]; //根据情况做适当修改
ll M=m[0],ans=a[0],g,x,y; //M是m[0..i]的最小公倍数
repeat(i,1,n){
ll c=((a[i]-ans)%m[i]+m[i])%m[i];
exgcd(M,m[i],g,x,y); //Ax=c(mod B)
if(c%g)return -1;
ans+=mul(x,c/g,m[i]/g)*M; //龟速乘
M*=m[i]/g;
ans=(ans%M+M)%M;
}
return (ans+M)%M;
}
离散对数 | BSGS+extra
- 求 \(a^x \equiv b \pmod m\) ,\(O(\sqrt m)\)
//BSGS,a和mod互质
ll bsgs(ll a,ll b,ll mod){ //a^ans%mod==b
a%=mod,b%=mod;
static unordered_map<ll,ll> m; m.clear();
ll t=(ll)sqrt(mod)+1,p=1;
repeat(i,0,t){
m[mul(b,p,mod)]=i; //p==a^i
p=mul(p,a,mod);
}
a=p; p=1;
repeat(i,0,t+1){
if(m.count(p)){ //p==a^i
ll ans=t*i-m[p];
if(ans>0)return ans;
}
p=mul(p,a,mod);
}
return -1;
}
//exBSGS,a和mod不需要互质,基于BSGS
ll exbsgs(ll a,ll b,ll mod){ //a^ans%mod==b
a%=mod,b%=mod;
if(b==1)return 0;
ll ans=0,c=1,g;
while((g=__gcd(a,mod))!=1){
if(b%g!=0)return -1;
b/=g,mod/=g;
c=mul(c,a/g,mod);
ans++;
if(b==c)return ans;
}
ll t=bsgs(a,mul(b,getinv(c,mod),mod),mod); //必须扩欧逆元!!
if(t==-1)return -1;
return t+ans;
}
阶与原根
- 判断是否有原根:若 \(m\) 有原根,则 \(m\) 一定是下列形式:\(2,4,p^a,2p^a\)( \(p\) 是奇素数, \(a\) 是正整数)
- 求所有原根:若 \(g\) 为 \(m\) 的一个原根,则 \(g^s\space(1\le s\le\varphi(m),\gcd(s,\varphi(m))=1)\) 给出了 \(m\) 的所有原根。因此若 \(m\) 有原根,则 \(m\) 有 \(\varphi(\varphi(m))\) 个原根
- 求一个原根,\(O(n\log\log n)\)
实际远远不到
ll getG(ll n){ //求n最小的原根
static vector<ll> a; a.clear();
ll t=0,k=n-1;
repeat(i,2,sqrt(k+1)+1)
if(k%i==0){
a.push_back(i); //a存放(n-1)的质因数
while(k%i==0)k/=i;
}
if(k!=1)a.push_back(k);
repeat(i,2,n){ //枚举答案
bool f=1;
for(auto j:a)
if(qpow(i,(n-1)/j,n)==1){
f=0;
break;
}
if(f)return i;
}
}
N次剩余
- 求 \(x^a \equiv b \pmod m\) ,基于BSGS、原根
//只求一个
ll residue(ll a,ll b,ll mod){ //ans^a%mod==b
ll g=getG(mod),c=bsgs(qpow(g,a,mod),b,mod);
if(c==-1)return -1;
return qpow(g,c,mod);
}
//求所有N次剩余
vector<ll> ans;
void allresidue(ll a,ll b,ll mod){ //ans^a%mod==b
ll g=getG(mod),c=bsgs(qpow(g,a,mod),b,mod);
ans.clear();
if(b==0){ans.push_back(0);return;}
if(c==-1)return;
ll now=qpow(g,c,mod);
ll step=(mod-1)/__gcd(a,mod-1);
ll ps=qpow(g,step,mod);
for(ll i=c%step;i<mod-1;i+=step,now=mul(now,ps,mod))
ans.push_back(now);
sort(ans.begin(),ans.end());
}
数论函数的生成
单个欧拉函数
- \(\varphi(n)=\) 小于
n
且与n
互质的正整数个数 - 令
n
的唯一分解式 \(n=Π({p_k}^{a_k})\),则 \(\varphi(n)=n\cdot Π(1-\dfrac 1 {p_k})\) - \(O(\sqrt n)\)
int euler_phi(int n){
int ans=n;
repeat(i,2,sqrt(n)+2)
if(n%i==0){
while(n%i==0)n/=i;
ans=ans/i*(i-1);
}
if(n>1)ans=ans/n*(n-1);
return ans;
}
线性递推乘法逆元
求1..(n-1)的逆元,\(O(n)\)
void get_inv(int n,int m=mod){
inv[1]=1;
repeat(i,2,n)
inv[i]=m-m/i*inv[m%i]%m;
}
求a[1..n]的逆元,离线,\(O(n)\)
void get_inv(int a[],int n){ //求a[1..n]的逆元,存在inv[1..n]中
static int pre[N];
pre[0]=1;
repeat(i,1,n+1)
pre[i]=(ll)pre[i-1]*a[i]%mod;
int inv_pre=qpow(pre[n],mod-2);
repeat_back(i,1,n+1){
inv[i]=(ll)pre[i-1]*inv_pre%mod;
inv_pre=(ll)inv_pre*a[i]%mod;
}
}
线性筛
- 定理:求出 \(f(p)\)(p为质数)的复杂度不超过 \(O(\log p)\) 的积性函数可以被线性筛
筛素数
- \(O(n)\)
//a[i]表示第i+1个质数,vis[i]==0表示i是素数
void get_prime(){
int cnt=0; vis[1]=1;
repeat(i,2,n+1){
if(!vis[i]) //是个质数
a[cnt++]=i; //记录质数
repeat(j,0,cnt){
if(i*a[j]>n)break;
vis[i*a[j]]=1;
if(i%a[j]==0)break; //此时a[j]是i的最小质因数
}
}
}
筛欧拉函数
- 线性版,\(O(n)\)
void get_phi(){
int cnt=0; /*vis[1]=1;*/ phi[1]=1;
repeat(i,2,n+1){
if(!vis[i])
a[cnt++]=i,phi[i]=i-1;
repeat(j,0,cnt){
if(i*a[j]>n)break;
vis[i*a[j]]=1;
if(i%a[j]==0){
phi[i*a[j]]=phi[i]*a[j];
break;
}
phi[i*a[j]]=phi[i]*(a[j]-1);
}
}
}
- 不是线性但节省力气和空间版,\(O(n\log\log n)\)
void get_phi(){
phi[1]=1; //其他的值初始化为0
repeat(i,2,n+1)
if(!phi[i])
for(int j=i;j<=n;j+=i){
if(!phi[j])phi[j]=j;
phi[j]=phi[j]/i*(i-1);
}
}
筛莫比乌斯函数
- \(O(n)\)
void get_mu(int n){
int cnt=0; /*vis[1]=1;*/ mu[1]=1;
repeat(i,2,n+1){
if(!vis[i])
a[cnt++]=i,mu[i]=-1;
repeat(j,0,cnt){
if(i*a[j]>n)break;
vis[i*a[j]]=1;
if(i%a[j]==0){
mu[i*a[j]]=0;
break;
}
mu[i*a[j]]=-mu[i];
}
}
}
杜教筛
- \(S(n)=\sum\limits_{i=1}^nf(i)\)
- 由公式
- \(\sum\limits_{i=1}^n\sum\limits_{d\mid i}g(d)f(\dfrac i d)=\sum\limits_{i=1}^n g(i)S(\lfloor\dfrac n i \rfloor)\)
- 右式拿出一项得
- \(g(1)S(n)=\sum\limits_{i=1}^n\sum\limits_{d\mid i}g(d)f(\dfrac i d)-\sum\limits_{i=2}^n g(i)S(\lfloor\dfrac n i \rfloor)\)
- 目前只有mu的杜教筛,\(O(n^{\tfrac 2 3})\)
struct DU{
static const int N=2000010;
map<ll,ll> mp_mu;
int sum_mu[N],a[N],mu[N];
bool vis[N];
ll S_mu(ll x){ //求mu的前缀和
if(x<N)return sum_mu[x];
if(mp_mu[x])return mp_mu[x];
ll ret=1;
for(ll i=2,j;i<=x;i=j+1){
j=x/(x/i);
ret-=S_mu(x/i)*(j-i+1);
}
return mp_mu[x]=ret;
}
void init(){
int cnt=0; /*vis[1]=1;*/ mu[1]=1;
repeat(i,2,N){
if(!vis[i])
a[cnt++]=i,mu[i]=-1;
repeat(j,0,cnt){
if(i*a[j]>=N)break;
vis[i*a[j]]=1;
if(i%a[j]==0){
mu[i*a[j]]=0;
break;
}
mu[i*a[j]]=-mu[i];
}
}
repeat(i,1,N)
sum_mu[i]=sum_mu[i-1]+mu[i];
}
}du;
素数约数相关
唯一分解
- 用数组表示数字唯一分解式的素数的指数,如 \(50=\{1,0,2,0,…\}\)
- 可以用来计算阶乘和乘除操作
void fac(int a[],ll n){
repeat(i,2,(int)sqrt(n)+2)
while(n%i==0)
a[i]++,n/=i;
if(n>1)a[n]++;
}
- set维护版
struct fac{
#define facN 1010
ll a[facN]; set<ll> s;
fac(){mst(a,0); s.clear();}
void lcm(ll n){ //self=lcm(self,n)
repeat(i,2,facN)
if(n%i==0){
int cnt=0;
while(n%i==0)
cnt++,n/=i;
a[i]=max(a[i],cnt); //改成a[i]+=cnt就变成了乘法
}
if(n>1)s.insert(n);
}
ll value(){ //求自己的模意义下的值
ll ans=1;
repeat(i,2,facN)
if(a[i])
ans=ans*qpow(i,a[i],mod)%mod;
for(auto i:s)
ans=ans*i%mod;
return ans;
}
}f;
素数判定 | 朴素 or Miller-Rabin
- 朴素算法,\(O(\sqrt n)\)
bool isprime(int n){
if(n<=3)return n>=2;
if(n%2==0 || n%3==0)return 0;
repeat(i,1,int(sqrt(n)+1.5)/6+1)
if(n%(i*6-1)==0 || n%(i*6+1)==0)return 0;
return 1;
}
- Miller-Rabin素性测试,\(O(10\cdot\log^3 n)\)
bool isprime(ll n){
if(n<4)return n>1;
ll a=n-1,b=0;
while(a%2==0)a/=2,++b;
repeat(i,0,10){
ll x=rnd()%(n-2)+2,v=qpow(x,a,n);
if(v==1 || v==n-1)continue;
repeat(j,0,b+1){
v=mul(v,v,n); //mul要防爆
if(v==n-1)break;
}
if(v!=n-1)return 0;
}
return 1;
}
大数分解 | Pollard-rho
- \(O(n^{\tfrac 1 4})\),基于MR素性测试(很遗憾的是,我不擅长卡常因此这个板子过不了洛谷P4718)
ll pollard_rho(ll c,ll n){
ll i=1,x,y,k=2,d;
x=y=rnd()%n;
while(1){
d=__gcd(n+y-x,n);
if(d>1 && d<n)
return d;
if(++i==k)y=x,k*=2;
x=(mul(x,x,n)+n-c)%n; //mul要防爆
if(y==x)return n;
}
}
vector<ll> ans; //存结果(质因数,无序)
void rho(ll n){ //分解n
if(isprime(n)){
ans.push_back(n);
return;
}
ll t;
do{t=pollard_rho(rnd()%(n-1)+1,n);}while(t>=n);
rho(t);
rho(n/t);
}
单个约数个数函数
int get_divisor(int n){ //求约数个数
int ans=0;
for(int i=1;i<n;i=n/(n/(i+1)))
if(n%i==0)
ans++; //v.push_back(i); //记录约数
return ans+1; //v.push_back(n); //记录约数
}
反素数生成
- 求因数最多的数(因数个数一样则取最小)
- 性质:\(M = {p_1}^{k_1}{p_2}^{k_2}...\) 其中,\(p_i\) 是从 \(2\) 开始的连续质数,\(k_i-k_{i+1}∈\{0,1\}\)
- 先打出质数表再 \(dfs\),枚举 \(k_n\),\(O(\exp)\)
int pri[16]={2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53};
ll n; //范围
pair<ll,ll> ans; //ans是结果,ans.fi是最大反素数,ans.se是反素数约数个数
void dfs(ll num=1,ll cnt=1,int *p=pri,int pre=inf){ //注意ans要初始化
if(make_pair(cnt,-num)>make_pair(ans.se,-ans.fi))
ans={num,cnt};
num*=*p;
for(int i=1;i<=pre && num<=n;i++,num*=*p)
dfs(num,p+1,i,cnt*(i+1));
}
\(n\) 以内约数个数最大值是 \(O(n^{\tfrac {1.066}{\ln\ln n}})\)
范围 | 1e4 | 1e5 | 1e6 | 1e9 | 1e16 |
---|---|---|---|---|---|
最大反素数 | 7560 | 83160 | 720720 | 735134400 | 8086598962041600 |
反素数约数个数 | 64 | 128 | 240 | 1344 | 41472 |
数论杂项
数论分块
\(n=(k-n\%k)(n/k)+(n\%k)(n/k+1)\)
将 \(\lfloor \dfrac{n}{x}\rfloor=C\) 的 \([x_{\min},x_{\max}]\) 作为一块,其中区间内的任一整数 \(x_0\) 满足 \(x_{\max}=n/(n/x_0)\)
for(int l=l0,r;l<=r0;l=r+1){
r=min(r0,n/(n/l));
//c=n/l;
//len=l-r+1;
}
将 \(\lceil \dfrac{n}{x}\rceil=C\) 的 \([x_{\min},x_{\max}]\) 作为一块:
for(int l=l0,r;l<=r0;l=r+1){
r=min(r0,n/(n/l)); if(n%r==0)r=max(r-1,l);
//c=(n+l-1)/l;
//len=l-r+1;
}
二次剩余
- 对于奇素数模数 \(p\),存在 \(\frac {p-1} 2\) 个二次剩余 \(\{1^2,2^2,...,(\frac {p-1} 2)^2\}\),和相同数量的二次非剩余
- 对于奇素数模数 \(p\),如果 \(n^{\frac{p-1}2}\equiv1\pmod{p}\) ,则 \(n\) 是一个二次剩余;如果 \(n^{\frac{p-1}2}\equiv-1\pmod{p}\),则 \(n\) 是一个二次非剩余
- 对于奇素数模数 \(p\),二次剩余的乘积是二次剩余,二次剩余与非二次剩余乘积为非二次剩余,非二次剩余乘积是二次剩余
- 费马-欧拉素数定理:\((4n+1)\) 型素数只能用一种方法表示为一个范数(两个完全平方数之和),\((4n+3)\) 型素数不能表示为一个范数
- 二次互反率:记 \(p^{\frac{q-1}2}\) 的符号为 \((\dfrac p q)\) ,则对奇素数 \(p,q\) 有 \((\dfrac p q)\cdot(\dfrac q p)=(-1)^{\tfrac{p-1}2\cdot\tfrac{q-1}2}\)
莫比乌斯反演
- 记\([P]=\begin{cases} 1&命题P为真\\0&命题P为假\end{cases}\)
- 引理1:\(\lfloor \dfrac{a}{bc}\rfloor=\lfloor \dfrac{\lfloor \frac{a}{b}\rfloor}{c}\rfloor\)
- 引理2:\(n\) 的因数个数 \(≤\lfloor 2\sqrt n \rfloor\)
- 数论分块:将 \(\lfloor \dfrac{n}{x}\rfloor=C\) 的 \([x_{\min},x_{\max}]\) 作为一块,其中区间内的任一整数 \(x_0\) 满足 \(x_{\max}=\lfloor\dfrac n{\lfloor\tfrac n x\rfloor}\rfloor\)
- def-积性函数:\(\gcd(x,y)=1\Rightarrow f(xy)=f(x)f(y)\)
- def-单位函数:\(\varepsilon(n)=[n=1]\)
- def-恒等函数:\(id(n)=n\)
- def-狄利克雷Dirichlet卷积:\((f*g)(n)=\sum\limits_{d|n}f(d)g(\dfrac n d)\)
- \(\varepsilon\) 为狄利克雷卷积的单位元
- 莫比乌斯函数性质:\(\mu(n)=\begin{cases} 1&n=1\\0&n含有平方因子\\(-1)^k&k为n的本质不同质因数个数\end{cases}\)
- 莫比乌斯函数是积性函数
- 超级重要结论:\(\varepsilon=\mu*1\),即\(\varepsilon(n)=\sum\limits_{d|n}\mu(d)\)
- 莫比乌斯反演公式:若\(f=g*1\),则\(g=f*\mu\);或者,若\(f(n)=\sum\limits_{d|n}g(d)\),则\(g(n)=\sum\limits_{d|n}\mu(d)f(\dfrac n d)\)
- (补充)欧拉函数性质:\(\varphi*1=id\)
- 例题:求模意义下的 \(\sum\limits_{i=1}^n \sum\limits_{j=1}^m \dfrac{i\cdot j}{\gcd(i,j)}\)
- \(ans=\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{d|i,d|j,\gcd(\frac i d,\frac j d)=1}\dfrac{i\cdot j}d\)
- 非常经典的化法:
- \(ans=\sum\limits_{d=1}^n d\cdot\sum\limits_{i=1}^{\lfloor\frac nd\rfloor}\sum\limits_{j=1}^{\lfloor\frac md\rfloor}[\gcd(i,j)=1]i\cdot j\)
- 设 \(sum(n,m)=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}[\gcd(i,j)=1]i\cdot j\)
- \(sum(n,m)=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}\sum\limits_{c|i,c|j}{\mu(c)}\cdot i\cdot j\)
- 设 \(i'=\dfrac i c,j'=\dfrac j c\)
- \(sum(n,m)=\sum\limits_{c=1}^n\mu(c)\cdot c^2\cdot\sum\limits_{i'=1}^{\lfloor\frac nc\rfloor}\sum\limits_{j'=1}^{\lfloor\frac mc\rfloor} i'\cdot j'\)
- 易得 \(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m} i\cdot j=\dfrac 1 4 n(n+1) m(m+1)\)
斐波那契数列
- 定义:\(F_0=0,F_1=1,F_n=F_{n-1}+F_{n-2}\)
- \(F_n=\dfrac 1 {\sqrt{5}} [(\dfrac 1 \Phi)^n-(-\Phi)^n)]\)
- \(F_{a+b-1}=F_{a-1}F_{b-1}+F_aF_b\) (重要公式)
- \(F_{n-1}F_{n+1}-F_n^2=(-1)^n\) (卡西尼性质)
- \(F_{n}^2+F_{n+1}^2=F_{2n+1}\)
- \(F_{n+1}^2-F_{n-1}^2=F_{2n}\) (由上一条写两遍相减得到)
- \(F_1+F_3+F_5+...+F_{2n-1}=F_{2n}\) (奇数项求和)
- \(F_2+F_4+F_6+...+F_{2n}=F_{2n+1}-1\) (偶数项求和)
- \(F_1^2+F_2^2+F_3^2+...+F_n^2=F_nF_{n+1}\)
- \(F_1+2F_2+3F_3+...+nF_n=nF_{n+2}-F_{n+3}+2\)
- \(-F_1+F_2-F_3+...+(-1)^nF_n=(-1)^n(F_{n+1}-F_n)+1\)
- \(F_{2n-2m-2}(F_{2n}+F_{2n+2})=F_{2m+2}+F_{4n-2m}\)
- \(F_a \mid F_b \Leftrightarrow a \mid b\)
- \(\gcd(F_a,F_b)=F_{\gcd(a,b)}\)
- 当 \(p\) 为 \(5k\pm 1\) 型素数时,\(\begin{cases} F_{p-1}\equiv 0\pmod p \\ F_p\equiv 1\pmod p \\ F_{p+1}\equiv 1\pmod p \end{cases}\)
- 当 \(p\) 为 \(5k\pm 2\) 型素数时,\(\begin{cases} F_{p-1}\equiv 1\pmod p \\ F_p\equiv -1\pmod p \\ F_{p+1}\equiv 0\pmod p \end{cases}\)
- \(F_{n+2}\) 为集合
{1,2,3,...,n-2}
中不包含相邻正整数的子集个数(包括空集) F(n)%m
的周期 \(\le 6m\) ( \(m=2\times 5^k\) 取等号)
快速倍增法求\(F_n\),返回二元组\((F_n,F_{n+1})\) ,\(O(\log n)\)
pii fib(ll n){ //fib(n).fi即结果
if(n==0)return {0,1};
pii p=fib(n>>1);
ll a=p.fi,b=p.se;
ll c=a*(2*b-a)%mod;
ll d=(a*a+b*b)%mod;
if(n&1)return {d,(c+d)%mod};
else return {c,d};
}
组合数学
组合数取模 | Lucas+extra
Lucas定理用来求模意义下的组合数
真·Lucas,\(p\) 是质数(后面的exLucas都不纯正)
ll lucas(ll a,ll b,ll p){ //a>=b
if(b==0)return 1;
return mul(c(a%p,b%p,p),lucas(a/p,b/p,p),p);
}
特例:如果p=2,可能lucas失效(?)
ll C(ll a,ll b){ //a>=b,p=2的情况
return (a&b)==b;
}
快速阶乘和exLucas
- \(qfac.A(x),qfac.B(x)\) 满足 \(A\equiv \dfrac{x!}{p^B}\pmod {p^k}\)
- \(qfac.C(a,b)\equiv C_a^b \pmod {p^k}\)
- \(exlucas(a,b,m)\equiv C_a^b \pmod m\),函数内嵌中国剩余定理
struct Qfac{
ll s[2000010];
ll p,m;
ll A(ll x){ //快速阶乘的A值
if(x==0)return 1;
ll c=A(x/p);
return s[x%m]*qpow(s[m],x/m,m)%m*c%m;
}
ll B(ll x){ //快速阶乘的B值
int ans=0;
for(ll i=x;i;i/=p)ans+=i/p;
return ans;
}
ll C(ll a,ll b){ //组合数,a>=b
ll k=B(a)-B(b)-B(a-b);
return A(a)*gcdinv(A(b),m)%m
*gcdinv(A(a-b),m)%m
*qpow(p,k,m)%m;
}
void init(ll _p,ll _m){ //初始化,一定要满足m=p^k
p=_p,m=_m;
s[0]=1;
repeat(i,1,m+1)
if(i%p)s[i]=s[i-1]*i%m;
else s[i]=s[i-1];
}
}qfac;
ll exlucas(ll a,ll b,ll mod){
ll ans=0,m=mod;
for(ll i=2;i<=m;i++) //不能repeat
if(m%i==0){
ll p=i,k=1;
while(m%i==0)m/=i,k*=i;
qfac.init(p,k);
ans=(ans+qfac.C(a,b)*(mod/k)%mod*gcdinv(mod/k,k)%mod)%mod;
}
return (ans+mod)%mod;
}
线性递推组合数
- \(O(n)\) 初始化,\(O(1)\) 查询
struct CC{
static const int N=100010;
ll fac[N],inv[N];
CC(){
fac[0]=1;
repeat(i,1,N)fac[i]=fac[i-1]*i%mod;
inv[N-1]=qpow(fac[N-1],mod-2,mod);
repeat_back(i,0,N-1)inv[i]=inv[i+1]*(i+1)%mod;
}
ll operator()(ll a,ll b){ //a>=b
if(a<b)return 0;
return fac[a]*inv[a-b]%mod*inv[b]%mod;
}
}C;
线性递推二项式系数
C(n,k)=(n-k+1)*C(n,k-1)/k
组合数学函数
- Catalan数,\(H_n=\dfrac{\binom{2n}n}{n+1}\),\(H_n=\dfrac{H_{n-1}(4n-2)}{n+1}\)
- Bell数,划分n个元素的集合的方案数
B[0]=B[1]=1;
repeat(i,2,N){
B[i]=0;
repeat(j,0,i)
B[i]=(B[i]+C(i-1,j)*B[j]%mod)%mod;
}
第一类Stirling数
- 多项式 \(x(x-1)(x-2) \cdots (x-n+1)\) 展开后 \(x^r\) 的系数绝对值记作 \(s(n,r)\) (系数符号 \((-1)^{n+r}\))
- 递推式 \(s(n,r) = (n-1)s(n-1,r)+s(n-1,r-1)\)
第二类Stirling数
- \(n\) 个不同的球放入 \(r\) 个相同的盒子且无空盒,记作 \(S(n,r)\) 或 \(S_n^r\)
- 递推式 \(S(n,r) = r S(n-1,r) + S(n-1,r-1)\)
康托展开+逆 编码与解码
康托展开+逆
- 排列里的元素都是从1到n
//普通版,O(n^2)
int cantor(int a[],int n){
int f=1,ans=1; //假设答案最小值是1
repeat_back(i,0,n){
int cnt=0;
repeat(j,i+1,n)
cnt+=a[j]<a[i];
ans=(ans+f*cnt%mod)%mod; //ans+=f*cnt;
f=f*(n-i)%mod; //f*=(n-i);
}
return ans;
}
//树状数组优化版,基于树状数组,O(nlogn)
int cantor(int a[],int n){
static BIT t; t.init(); //树状数组
ll f=1,ans=1; //假设答案最小值是1
repeat_back(i,0,n){
ans=(ans+f*t.sum(a[i])%mod)%mod; //ans+=f*t.sum(a[i]);
t.add(a[i],1);
f=f*(n-i)%mod; //f*=(n-i);
}
return ans;
}
//逆展开普通版,O(n^2)
int *decantor(int x,int n){
static int f[13]={1};
repeat(i,1,13)f[i]=f[i-1]*i;
static int ans[N];
set<int> s;
x--;
repeat(i,1,n+1)s.insert(i);
repeat(i,0,n){
int q=x/f[n-i-1];
x%=f[n-i-1];
auto it=s.begin();
repeat(i,0,q)it++; //第q+1小的数
ans[i]=*it;
s.erase(it);
}
return ans;
}
编码与解码问题
<1>
- 给定一个字符串,求出它的编号
- 例,输入acab,输出5(aabc,aacb,abac,abca,acab,...)
- 用递归,令d(S)是小于S的排列数,f(S)是S的全排列数
- 小于acab的第一个字母只能是a,所以d(acab)=d(cab)
- 第二个字母是a,b,c,所以d(acab)=f(bc)+f(ac)+d(ab)
- d(ab)=0
- 因此d(acab)=4,加1之后就是答案
<2>
- 给定编号求字符串,对每一位进行尝试即可
置换群计数
Polya定理
- 例:立方体 \(n=6\) 个面,每个面染上 \(m=3\) 种颜色中的一种
- 两个染色方案相同意味着两个立方体经过旋转可以重合
- 其染色方案数为:\(\dfrac{\sum m^{k_i}}{|k|}\)(\(k_i\) 为某一置换可以拆分的循环置换数,\(|k|\) 为所有置换数)
不旋转,{U|D|L|R|F|B},k=6,共1个
对面中心连线为轴的90度旋转,{U|D|L R F B},k=3,共6个
对面中心连线为轴的180度旋转,{U|D|L R|F B},k=4,共3个
对棱中点连线为轴的180度旋转,{U L|D R|F B},k=3,共6个
对顶点连线为轴的120度旋转,{U L F|D R B},k=2,共8个
- 因此 \(\dfrac{3^6+3^3 \cdot 6+3^4 \cdot 3+3^3 \cdot 6+3^2 \cdot 8}{1+6+3+6+8}=57\)
- 例题(poj1286),n个点连成环,染3种颜色,允许旋转和翻转
ans=0,cnt=0;
//只考虑旋转,不考虑翻转
repeat(i,1,n+1)
ans+=qpow(3,__gcd(i,n));
cnt+=n;
//考虑翻转
if(n%2==0)ans+=(qpow(3,n/2+1)+qpow(3,n/2))*(n/2);
else ans+=qpow(3,(n+1)/2)*n;
cnt+=n;
cout<<ans/cnt<<endl;
组合数学的一些结论
- 一个长为 \(n+m\) 的数组,\(n\) 个 \(1\),\(m\) 个 \(-1\),限制前缀和最大为 \(k\),则方案数为 \(C_{n+m}^{m+k}-C_{n+m}^{m+k+1}\)
博弈论
SG定理
- 有向无环图中,两个玩家轮流推一个棋子,不能走的判负
- 假设x的k个后继状态
{yi}
SG(x)=mex{SG(yi)}
,mex(S)=
不属于集合S的最小自然数- 当且仅当
XOR{SG(起点1),SG(起点2),...}
为0时先手必败 - (如果只有一个起点,SG的值可以只考虑01)
- 例题:拿n堆石子,每次只能拿一堆中的斐波那契数颗石子
void getSG(int n){
mst(SG,0);
repeat(i,1,n+1){
mst(S,0);
for(int j=0;f[j]<=i && j<=N;j++)
S[SG[i-f[j]]]=1;
for(int j=0;;j++)
if(!S[j]){
SG[i]=j;
break;
}
}
}
Nim
- n堆石子,分别有
ai
颗石子 - 两人每次可以拿任意堆任意非空的石子,拿不了的人判负
- 解:
NimSum=XOR{ai}
,当且仅当NimSum
为0时先手必败(SG证) - 注:先手必胜策略是找到任意第
(63-__builtin_clzll(NimSum))
位是1的a[i],并取走a[i]^NimSum
个石子
Nimk
- 每次最多选取k堆石子,选中的每一堆都取走任意非空的石子
- 解:当且仅当下述命题成立,先手必胜
- 存在t使得
sum(ai & (1<<t))%(k+1)!=0
多项式
拉格朗日插值
- 函数曲线通过n个点 \((x_i,y_i)\),求 \(f(k)\)
- 拉格朗日插值:\(f(x)=\sum\limits_{i=1}^n[y_i\Pi_{j!=i}\dfrac{x-x_j}{x_i-x_j}]\)
- \(O(n^2)\)
repeat(i,0,n)x[i]%=mod,y[i]%=mod;
repeat(i,0,n){
s1=y[i];
s2=1;
repeat(j,0,n)
if(i!=j){
s1=s1*(k-x[j])%mod;
s2=s2*(x[i]-x[j])%mod;
}
ans=(ans+s1*getinv(s2)%mod+mod)%mod;
}
快速傅里叶变换+任意模数
- 求两个多项式的卷积,\(O(n\log n)\)
struct FFT{
static const int N=1<<20;
struct cp{
long double a,b;
cp(){}
cp(const long double &a,const long double &b):a(a),b(b){}
cp operator+(const cp &t)const{return cp(a+t.a,b+t.b);}
cp operator-(const cp &t)const{return cp(a-t.a,b-t.b);}
cp operator*(const cp &t)const{return cp(a*t.a-b*t.b,a*t.b+b*t.a);}
cp conj()const{return cp(a,-b);}
};
cp wn(int n,int f){
static const long double pi=acos(-1.0);
return cp(cos(pi/n),f*sin(pi/n));
}
int g[N];
void dft(cp a[],int n,int f){
repeat(i,0,n)if(i>g[i])swap(a[i],a[g[i]]);
for(int i=1;i<n;i<<=1){
cp w=wn(i,f);
for(int j=0;j<n;j+=i<<1){
cp e(1,0);
for(int k=0;k<i;e=e*w,k++){
cp x=a[j+k],y=a[j+k+i]*e;
a[j+k]=x+y,a[j+k+i]=x-y;
}
}
}
if(f==-1){
cp Inv(1.0/n,0);
repeat(i,0,n)a[i]=a[i]*Inv;
}
}
#ifdef CONV
cp a[N],b[N];
vector<ll> conv(const vector<ll> &u,const vector<ll> &v){ //一般fft
const int n=(int)u.size()-1,m=(int)v.size()-1;
const int k=32-__builtin_clz(n+m+1),s=1<<k;
g[0]=0; repeat(i,1,s)g[i]=(g[i/2]/2)|((i&1)<<(k-1));
repeat(i,0,s){
a[i]=cp(i<=n?u[i]:0,0);
b[i]=cp(i<=m?v[i]:0,0);
}
dft(a,s,1); dft(b,s,1);
repeat(i,0,s)a[i]=a[i]*b[i];
dft(a,s,-1);
vector<ll> ans;
repeat(i,0,n+m+1)ans+=llround(a[i].a);
return ans;
}
#endif
#ifdef CONV_MOD
cp a[N],b[N],Aa[N],Ab[N],Ba[N],Bb[N];
vector<ll> conv_mod(const vector<ll> &u,const vector<ll> &v,ll mod){ //任意模数fft
const int n=(int)u.size()-1,m=(int)v.size()-1,M=sqrt(mod)+1;
const int k=32-__builtin_clz(n+m+1),s=1<<k;
g[0]=0; repeat(i,1,s)g[i]=(g[i/2]/2)|((i&1)<<(k-1));
repeat(i,0,s){
a[i]=i<=n?cp(u[i]%mod%M,u[i]%mod/M):cp();
b[i]=i<=m?cp(v[i]%mod%M,v[i]%mod/M):cp();
}
dft(a,s,1); dft(b,s,1);
repeat(i,0,s){
int j=(s-i)%s;
cp t1=(a[i]+a[j].conj())*cp(0.5,0);
cp t2=(a[i]-a[j].conj())*cp(0,-0.5);
cp t3=(b[i]+b[j].conj())*cp(0.5,0);
cp t4=(b[i]-b[j].conj())*cp(0,-0.5);
Aa[i]=t1*t3,Ab[i]=t1*t4,Ba[i]=t2*t3,Bb[i]=t2*t4;
}
repeat(i,0,s){
a[i]=Aa[i]+Ab[i]*cp(0,1);
b[i]=Ba[i]+Bb[i]*cp(0,1);
}
dft(a,s,-1); dft(b,s,-1);
vector<ll> ans;
repeat(i,0,n+m+1){
ll t1=llround(a[i].a)%mod;
ll t2=llround(a[i].b)%mod;
ll t3=llround(b[i].a)%mod;
ll t4=llround(b[i].b)%mod;
ans+=(t1+(t2+t3)*M%mod+t4*M*M)%mod;
}
return ans;
}
#endif
}fft;
多项式的一些概念
- 生成函数:\(A(x)=a_0+a_1x+a_2x^2+...\)
- 组合对象:x
- 组合对象的大小:x的指数i
- 方案数:系数
- 有 \(1+x+x^2+...=\dfrac{1}{1-x}\)
- 指数生成函数:无序排列
- 有 \(1+x+\dfrac{x^2}{2!}+\dfrac{x^3}{3!}+...=e^x\)
- 严重空缺
线性代数
矩阵乘法 矩阵快速幂
struct mat{
static const int N=110;
ll a[N][N];
explicit mat(int e=0){
repeat(i,0,n)
repeat(j,0,n)
a[i][j]=e*(i==j);
}
mat operator*(const mat &b)const{ //矩阵乘法
mat ans(0);
repeat(i,0,n)
repeat(j,0,n){
ll &t=ans.a[i][j];
repeat(k,0,n)
t=(t+a[i][k]*b.a[k][j])%mod;
}
return ans;
}
};
mat qpow(mat a,ll b){ //矩阵快速幂
mat ans(1);
while(b){
if(b&1)ans=ans*a;
a=a*a;
b>>=1;
}
return ans;
}
矩阵高级操作
- 行列式、逆矩阵(luogu P3389 && luogu P4783)
- \(O(n^3)\)
int n,m;
#define T lf
struct mat{
static const int N=110;
vector< vector<T> > a;
mat():a(N,vector<T>(N*2)){} //如果要求逆这里乘2
T det;
void r_div(int x,T k){ //第x行除以实数k
T r=getinv(k,mod);
repeat(i,0,m) //从x开始也没太大关系(对求det来说)
a[x][i]=a[x][i]*r%mod;
det=det*k%mod;
}
void r_plus(int x,int y,T k){ //第x行加上第y行的k倍
repeat(i,0,m)
a[x][i]=(a[x][i]+a[y][i]*k)%mod;
}
/*
void r_div(int x,T k){ //lf版
T r=1/k;
repeat(i,0,m)
a[x][i]*=r;
det*=k;
}
void r_plus(int x,int y,T k){ //lf版
repeat(i,0,m)
a[x][i]+=a[y][i]*k;
}
*/
bool gauss(){ //高斯消元,返回是否满秩,注意必须n<=m
det=1;
repeat(i,0,n){
int t=-1;
repeat(j,i,n)
if(abs(a[j][i])>err){
t=j;
break;
}
if(t==-1){det=0; return 0;}
if(t!=i){
a[i].swap(a[t]);
det=-det;
}
r_div(i,a[i][i]);
repeat(j,0,n) //如果只要det可以从i+1开始
if(j!=i && abs(a[j][i])>err)
r_plus(j,i,-a[j][i]);
}
return 1;
}
T get_det(){ //返回行列式
gauss();
return det;
}
bool get_inv(){ //把自己变成逆矩阵,返回是否成功
if(n!=m)return 0;
repeat(i,0,n)
repeat(j,0,n)
a[i][j+n]=i==j; //生成增广矩阵
m*=2;
bool t=gauss();
m/=2;
repeat(i,0,n)
repeat(j,0,n)
a[i][j]=a[i][j+n];
return t;
}
}a;
- 任意模数行列式(HDOJ 2827)
- \(O(n^3\log C)\)
int n;
struct mat{
static const int N=110;
vector< vector<ll> > a;
mat():a(N,vector<ll>(N)){}
ll det(int n){
ll ans=1;
repeat(i,0,n){
repeat(j,i+1,n)
while(a[j][i]){
ll t=a[i][i]/a[j][i];
repeat(k,i,n)a[i][k]=(a[i][k]-a[j][k]*t)%mod;
swap(a[i],a[j]);
ans=-ans;
}
ans=ans*a[i][i]%mod;
if(!ans)return 0;
}
return (ans+mod)%mod;
}
}a;
矩阵的一些结论
- \(n\times n\) 方阵 \(A\) 有:\(\left[\begin{array}{c}A&E\\O&E\end{array}\right]^{k+1}=\left[\begin{array}{c}A^k&E+A+A^2+...+A^k\\O&E\end{array}\right]\)
- 线性递推转矩快
- \(f_{n+3}=af_{n+2}+bf_{n+1}+cf_{n}\\\Leftrightarrow\left[\begin{array}{c}a&b&c\\1&0&0\\0&1&0\end{array}\right]^n \left[\begin{array}{c}f_2\\f_1\\f_0\end{array}\right]=\left[\begin{array}{c}f_{n+2}\\f_{n+1}\\f_{n}\end{array}\right]\)
线性基
- 线性基是一系列线性无关的基向量组成的集合
异或线性基
- 插入、查询 \(O(\log M)\)
struct basis{
static const int n=63;
#define B(x,i) ((x>>i)&1)
ll a[n],sz;
bool failpush; //是否线性相关
void init(){mst(a,0); sz=failpush=0;}
void push(ll x){ //插入元素
repeat(i,0,n)
if(B(x,i))
x^=a[i];
if(x!=0){
int p=63-__builtin_clzll(x);
sz++;
repeat(i,p+1,n)
if(B(a[i],p))
a[i]^=x;
a[p]=x;
}
else failpush=1;
}
ll top(){ //最大值
ll ans=0;
repeat(i,0,n)
ans^=a[i];
return ans;
}
bool exist(ll x){ //是否存在
repeat_back(i,0,n)
if((x>>i)&1){
if(a[i]==0)return 0;
else x^=a[i];
}
return 1;
}
ll kth(ll k){ //第k小,不存在返回-1
if(failpush)k--; //如果认为0是可能的答案就加这句话
if(k>=(1ll<<sz))return -1;
ll ans=0;
repeat(i,0,n)
if(a[i]!=0){
if(k&1)ans^=a[i];
k>>=1;
}
return ans;
}
}b;
basis operator+(basis a,const basis &b){ //将b并入a
repeat(i,0,a.n)
if(b.a[i])
a.push(b.a[i]);
a.failpush|=b.failpush;
return a;
}
- 这个版本中求kth需要rebuild \(O(\log^2 n)\)
struct basis{
...
void push(ll x){ //插入元素
repeat_back(i,0,n)
if((x>>i)&1){
if(a[i]==0){a[i]=x; sz++; return;}
else x^=a[i];
}
failpush=1;
}
ll top(){ //最大值
ll ans=0;
repeat_back(i,0,n)
ans=max(ans,ans^a[i]);
return ans;
}
void rebuild(){ //求第k小的前置操作
repeat_back(i,0,n)
repeat_back(j,0,i)
if((a[i]>>j)&1)
a[i]^=a[j];
}
}b;
实数线性基
- 编号从 \(0\) 开始,插入、查询 \(O(n^2)\)
struct basis{
lf a[N][N]; bool f[N]; int n; //f[i]表示向量a[i]是否被占
void init(int _n){
n=_n;
fill(f,f+n,0);
}
bool push(lf x[]){ //返回0表示可以被线性表示,不需要插入
repeat(i,0,n)
if(fabs(x[i])>1e-5){ //这个值要大一些
if(f[i]){
lf t=x[i]/a[i][i];
repeat(j,0,n)
x[j]-=t*a[i][j];
}
else{
f[i]=1;
repeat(j,0,n)
a[i][j]=x[j];
return 1;
}
}
return 0;
}
}b;
数学杂项
主定理
- 对于 \(T(n)=aT(\dfrac nb)+n^k\) (要估算 \(n^k\) 的 \(k\) 值)
- 若 \(\log_ba>k\),则 \(T(n)=O(n^{\log_ba})\)
- 若 \(\log_ba=k\),则 \(T(n)=O(n^k\log n)\)
- 若 \(\log_ba<k\)(有省略),则 \(T(n)=O(n^k)\)
质数表
42737, 46411, 50101, 52627, 54577, 191677, 194869, 210407, 221831, 241337, 578603, 625409, 713569, 788813, 862481, 2174729, 2326673, 2688877, 2779417, 3133583, 4489747, 6697841, 6791471, 6878533, 7883129, 9124553, 10415371, 11134633, 12214801, 15589333, 17148757, 17997457, 20278487, 27256133, 28678757, 38206199, 41337119, 47422547, 48543479, 52834961, 76993291, 85852231, 95217823, 108755593, 132972461, 171863609, 173629837, 176939899, 207808351, 227218703, 306112619, 311809637, 322711981, 330806107, 345593317, 345887293, 362838523, 373523729, 394207349, 409580177, 437359931, 483577261, 490845269, 512059357, 534387017, 698987533, 764016151, 906097321, 914067307, 954169327
1572869, 3145739, 6291469, 12582917, 25165843, 50331653 (适合哈希的素数)
19260817 原根15,是某个很好用的质数
1000000007 原根5
998244353 原根3
- NTT素数表, \(g\) 是模 \((r \cdot 2^k+1)\) 的原根
r*2^k+1 r k g
3 1 1 2
5 1 2 2
17 1 4 3
97 3 5 5
193 3 6 5
257 1 8 3
7681 15 9 17
12289 3 12 11
40961 5 13 3
65537 1 16 3
786433 3 18 10
5767169 11 19 3
7340033 7 20 3
23068673 11 21 3
104857601 25 22 3
167772161 5 25 3
469762049 7 26 3
998244353 119 23 3
1004535809 479 21 3
2013265921 15 27 31
2281701377 17 27 3
3221225473 3 30 5
75161927681 35 31 3
77309411329 9 33 7
206158430209 3 36 22
2061584302081 15 37 7
2748779069441 5 39 3
6597069766657 3 41 5
39582418599937 9 42 5
79164837199873 9 43 5
263882790666241 15 44 7
1231453023109121 35 45 3
1337006139375617 19 46 3
3799912185593857 27 47 5
4222124650659841 15 48 19
7881299347898369 7 50 6
31525197391593473 7 52 3
180143985094819841 5 55 6
1945555039024054273 27 56 5
4179340454199820289 29 57 3
struct of 自动取模
- 不好用,别用了
struct mint{
ll v;
mint(ll _v){v=_v%mod;}
mint operator+(const mint &b)const{return v+b.v;}
mint operator-(const mint &b)const{return v-b.v;}
mint operator*(const mint &b)const{return v*b.v;}
explicit operator ll(){return (v+mod)%mod;}
};
struct of 高精度
- 加、减、乘、单精度取模、小于号和等于号(其他不等号用rel_ops命名空间)
- 如果涉及除法,
那就完蛋,用java吧;如果不想打这么多行也用java吧(一定要让队友会写java)
struct big{
vector<ll> a;
static const ll k=1000000000,w=9;
int size()const{return a.size();}
explicit big(const ll &x=0){ //接收ll
*this=big(to_string(x));
}
explicit big(const string &s){ //接收string
static ll p10[9]={1};
repeat(i,1,w)p10[i]=p10[i-1]*10;
int len=s.size();
int f=(s[0]=='-')?-1:1;
a.resize(len/w+1);
repeat(i,0,len-(f==-1))
a[i/w]+=f*(s[len-1-i]-48)*p10[i%w];
adjust();
}
int sgn(){return a.back()>=0?1:-1;} //这个只能在强/弱调整后使用
void shrink(){ //收缩(内存不收缩)
while(size()>1 && a.back()==0)a.pop_back();
}
void adjust(){ //弱调整
repeat(i,0,3)a.push_back(0);
repeat(i,0,size()-1){
a[i+1]+=a[i]/k;
a[i]%=k;
}
shrink();
}
void final_adjust(){ //强调整
adjust();
int f=sgn();
repeat(i,0,size()-1){
ll t=(a[i]+k*f)%k;
a[i+1]+=(a[i]-t)/k;
a[i]=t;
}
shrink();
}
operator string(){ //转换成string
static char s[N]; char *p=s;
final_adjust();
if(sgn()==-1)*p++='-';
repeat_back(i,0,size())
sprintf(p,i==size()-1?"%lld":"%09lld",abs(a[i])),p+=strlen(p);
return s;
}
const ll &operator[](int n)const{ //访问
return a[n];
}
ll &operator[](int n){ //弹性访问
repeat(i,0,n-size()+1)a.push_back(0);
return a[n];
}
};
big operator+(big a,const big &b){
repeat(i,0,b.size())a[i]+=b[i];
a.adjust();
return a;
}
big operator-(big a,const big &b){
repeat(i,0,b.size())a[i]-=b[i];
a.adjust();
return a;
}
big operator*(const big &a,const big &b){
big ans;
repeat(i,0,a.size()){
repeat(j,0,b.size())
ans[i+j]+=a[i]*b[j];
ans.adjust();
}
return ans;
}
ll operator%(const big &a,ll mod){
ll ans=0,p=1;
repeat(i,0,a.size()){
ans=(ans+p*a[i])%mod;
p=(p*a.k)%mod;
}
return (ans+mod)%mod;
}
bool operator<(big a,big b){
a.final_adjust();
b.final_adjust();
repeat_back(i,0,max(a.size(),b.size()))
if(a[i]!=b[i])return a[i]<b[i];
return 0;
}
bool operator==(big a,big b){
a.final_adjust();
b.final_adjust();
repeat_back(i,0,max(a.size(),b.size()))
if(a[i]!=b[i])return 0;
return 1;
}
表达式求值
inline int lvl(const string &c){ //运算优先级,小括号要排最后
if(c=="*")return 2;
if(c=="(" || c==")")return 0;
return 1;
}
string convert(const string &in) { //中缀转后缀
stringstream ss;
stack<string> op;
string ans,s;
repeat(i,0,in.size()-1){
ss<<in[i];
if(!isdigit(in[i]) || !isdigit(in[i+1])) //插入空格
ss<<" ";
}
ss<<in.back();
while(ss>>s){
if(isdigit(s[0]))ans+=s+" ";
else if(s=="(")op.push(s);
else if(s==")"){
while(!op.empty() && op.top()!="(")
ans+=op.top()+" ",op.pop();
op.pop();
}
else{
while(!op.empty() && lvl(op.top())>=lvl(s))
ans+=op.top()+" ",op.pop();
op.push(s);
}
}
while(!op.empty())ans+=op.top()+" ",op.pop();
return ans;
}
ll calc(const string &in){ //后缀求值
stack<ll> num;
stringstream ss;
ss<<in;
string s;
while(ss>>s){
char c=s[0];
if(isdigit(c))
num.push((stoll(s))%mod);
else{
ll b=num.top(); num.pop();
ll a=num.top(); num.pop();
if(c=='+')num.push((a+b)%mod);
if(c=='-')num.push((a-b)%mod);
if(c=='*')num.push((a*b)%mod);
//if(c=='^')num.push(qpow(a,b));
}
}
return num.top();
}
一些数学结论
约瑟夫问题
- n个人编号0..(n-1),每次数到k出局,求最后剩下的人的编号
- 线性算法,\(O(n)\)
int jos(int n,int k){
int res=0;
repeat(i,1,n+1)res=(res+k)%i;
return res; //res+1,如果编号从1开始
}
- 对数算法,适用于k较小情况,\(O(k\log n)\)
int jos(int n,int k){
if(n==1 || k==1)return n-1;
if(k>n)return (jos(n-1,k)+k)%n; //线性算法
int res=jos(n-n/k,k)-n%k;
if(res<0)res+=n; //mod n
else res+=res/(k-1); //还原位置
return res; //res+1,如果编号从1开始
}
格雷码 汉诺塔
格雷码
- 一些性质:
- 相邻格雷码只变化一次
grey(n-1)
到grey(n)
修改了二进制的第(__builtin_ctzll(n)+1)
位grey(0)..grey(2^k-1)
是k维超立方体顶点的哈密顿回路,其中格雷码每一位代表一个维度的坐标- 格雷码变换,正 \(O(1)\),逆 \(O(logn)\)
ll grey(ll n){ //第n个格雷码
return n^(n>>1);
}
ll degrey(ll n){ //逆格雷码变换,法一
repeat(i,0,63) //or 31
n=n^(n>>1);
return n;
}
ll degrey(ll n){ //逆格雷码变换,法二
int ans=0;
while(n){
ans^=n;
n>>=1;
}
return ans;
}
汉诺塔
- 假设盘数为n,总共需要移动
(1<<n)-1
次 - 第k次移动第
i=__builtin_ctzll(n)+1
小的盘子 - 该盘是第
(k>>i)+1
次移动 - (可以算出其他盘的状态:总共移动了
((k+(1<<(i-1)))>>i)
次) - 该盘的移动顺序是:
A->C->B->A(当i和n奇偶性相同)
A->B->C->A(当i和n奇偶性不同)
cin>>n; //层数
repeat(k,1,(1<<n)){
int i=__builtin_ctzll(k)+1;
int p1=(k>>i)%3; //移动前状态
int p2=(p1+1)%3; //移动后状态
if(i%2==n%2){
p1=(3-p1)%3;
p2=(3-p2)%3;
}
cout<<"move "<<i<<": "<<"ABC"[p1]<<" -> "<<"ABC"[p2]<<endl;
}
Stern-Brocot树 Farey序列
- 分数序列:在 \([\dfrac 0 1,\dfrac 1 0]\) 中不断在 \(\dfrac a b\) 和 \(\dfrac c d\) 之间插入 \(\dfrac {a+c}{b+d}\)
- 性质:所有数都是既约分数、可遍历所有既约分数、保持单调递增
- Stern-Brocot树:二叉树,其第 \(k\) 行是分数序列第 \(k\) 次操作新加的数
- Farey序列:\(F_n\) 是所有分子分母 \(\le n\) 的既约分数按照分数序列顺序排列后的序列
- \(F_n\) 的长度 \(=1+\sum\limits_{i=1}^n\varphi(i)\)
浮点与近似计算
数值积分 | 自适应辛普森法
求 \(\int_{l}^{r}f(x)\mathrm{d}x\) 的近似值
lf raw(lf l,lf r){ //辛普森公式
return (f(l)+f(r)+4*f((l+r)/2))*(r-l)/6;
}
lf asr(lf l,lf r,lf err,lf ans){
lf m=(l+r)/2;
lf x=raw(l,m),y=raw(m,r);
if(fabs(x+y-ans)<=15*err)
return x+y-(x+y-ans)/15;
return asr(l,m,err/2,x)+asr(m,r,err/2,y);
}
//调用方法:asr(l,r,err,raw(l,r))
牛顿迭代法
- 求 \(f(x)\) 的零点:\(x_{n+1}=x_n-\dfrac{f(x)}{f'(x)}\)
- 检验 \(x_{n+1}=g(x_n)\) 多次迭代可以收敛于 \(x_0\) 的方法:看 \(|g'(x_0)|\le1\) 是否成立
lf newton(lf n){
lf x=1;
while(1){
lf y=(x+n/x)/2; //这里求了sqrt
if(fabs(x-y)<err)return x;
x=y;
}
}
- java高精度的整数平方根
public static BigInteger isqrtNewton(BigInteger n){
BigInteger a=BigInteger.ONE.shiftLeft(n.bitLength()/2);
boolean d=false;
while(true){
BigInteger b=n.divide(a).add(a).shiftRight(1);
if(a.compareTo(b)==0 || a.compareTo(b)<0 && d)
break;
d=a.compareTo(b)>0;
a=b;
}
return a;
}
others of 浮点与近似计算
- \(\lim\limits_{n\rightarrow\infty}\dfrac{错排(n)}{n!}=\dfrac 1 e,e\approx 2.718281828459045235360287471352\)
- \(\lim\limits_{n\rightarrow\infty}(\sum\frac 1 n-\ln n)=\gamma\approx 0.577215664901532860606\)
others of 数学杂项
- 埃及分数Engel展开
- 待展开的数为 \(x\),令 \(u_1=x ∧ u_{i+1}=u_i\times\lceil\dfrac 1 {u_i}\rceil-1\)(到0为止)
- 令 \(a_i=\lceil\dfrac 1 {u_i}\rceil\)
- 则 \(x=\dfrac 1{a_1}+\dfrac 1{a_1a_2}+\dfrac 1{a_1a_2a_3}+...\)
- 三个水杯容量为 \(a,b,c\)(正整数),\(a=b+c\),初始 \(a\) 装满水,则得到容积为 \(\dfrac a 2\) 的水需要倒 \(\dfrac a{\gcd(b,c)}-1\) 次水
- 兰顿蚂蚁(白色异或右转,黑色异或左转),约一万步后出现周期为104步的无限重复(高速公路)
- 任意勾股数能由复数 \((a+bi)^2\space(a,b∈\Z)\) 得到
- 任意正整数 \(a\) 都存在正整数 \(b,c\) 使得 \(a<b<c\) 且 \(a^2,b^2,c^2\) 成等差数列:构造 \(b=5a,c=7a\)
- 任意有理数 \(a\) 可以写成三个有理数的立方和 \(a=(\dfrac{a^3-3^6}{3^2a^2+3^4a+3^6})^3+(\dfrac{-a^3+3^5a+3^6}{3^2a^2+3^4a+3^6})^3+(\dfrac{3^3a^2+3^5a}{3^2a^2+3^4a+3^6})^3\)
- 拉格朗日四平方和定理:每个正整数都能表示为4个整数平方和
- 对于偶素数 \(2\) 有 \(2=1^2+1^2+0^2+0^2\)
- 对于奇素数 \(p\) 有 \(p=a^2+b^2+1^2+0^2\) (容斥可证)
- 对于所有合数 \(n\) 有 \(n=z_1^2+z_2^2+z_3^2+z_4^2=(x_1^2+x_2^2+x_3^2+x_4^2)\cdot(y_1^2+y_2^2+y_3^2+y_4^2)\)
- 其中 \(\begin{cases} z_1=x_1y_1+x_2y_2+x_3y_3+x_4y_4 \\ z_2=x_1y_2-x_2y_1-x_3y_4+x_4y_3 \\ z_3=x_1y_3-x_3y_1+x_2y_4-x_4y_2 \\ z_4=x_1y_4-x_4y_1-x_2y_3+x_3y_2\end{cases}\)