模板合集
快读快写
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9')
x=x*10+ch-'0',ch=getchar();
return x*f;
}
void write(int x)
{
if(x<0)
putchar('-'),x=-x;
if(x>9)
write(x/10);
putchar(x%10+'0');
return;
}
位运算
// 获取 a 的第 b 位,最低位编号为 0
int getBit(int a, int b) { return (a >> b) & 1; }
// 将 a 的第 b 位设置为 0 ,最低位编号为 0
int unsetBit(int a, int b) { return a & ~(1 << b); }
// 将 a 的第 b 位设置为 1 ,最低位编号为 0
int setBit(int a, int b) { return a | (1 << b); }
// 将 a 的第 b 位取反 ,最低位编号为 0
int flapBit(int a, int b) { return a ^ (1 << b); }
popcount
// 求 x 的汉明权重(二进制位中1的个数)
int popcount(int x)
{
int cnt = 0;
while (x)
{
cnt++;
x -= x & -x;
}
return cnt;
}
构造汉明权重递增的排列(O(n))
我们知道,一个汉明权重为 𝑛 的最小的整数为 2𝑛 −1.只要可以在常数时间构造出一个整数汉明权重相等的后继,我们就可以通过枚举汉明权重,从 2𝑛 −1 开始不断寻找下一个数的方式,在 𝑂(𝑛)时间内构造出 \(0\sim n\) 的符合要求的排列.
而找出一个数 𝑥 汉明权重相等的后继有这样的思路,以 \((10110)_2\) 为例:
-
把 \((10110)_2\) 最右边的 1 向左移动,如果不能移动,移动它左边的 1,以此类推,得到 \((11010)_2\)
-
把得到的 \((11010)_2\) 最后移动的 1 原先的位置一直到最低位的所有 1 都移到最右边.这里最后移动的 1 原来在第三位,所以最后三位 010 要变成 001,得到 \((11001)_2\).
这个过程可以用位运算优化:
int t = x + (x & -x);
x = t | ((((t&-t)/(x&-x))>>1)-1);
- 第一个步骤中,我们把数 𝑥 加上它的
lowbit,在二进制表示下,就相当于把 𝑥 最右边的连续一段 1 换成它左边的一个 1.如刚才提到的二进制数 \((10110)_2\),它在加上它的lowbit后是 \((11000)_2\).这其实得到了我们答案的前半部分. - 我们接下来要把答案后面的 1 补齐,𝑡 的
lowbit是 𝑥 最右边连续一段 1 最左边的 1 移动后的位置,而 𝑥 的lowbit则是 𝑥 最右边连续一段 1 最右边的位置.还是以 \((10110)_2\) 为例,\(𝑡 =(11000)_2\) - 接下来的除法操作是这种位运算中最难理解的部分,但也是最关键的部分.我们设原数最右边连续一段 1 最高位的 1 在第 𝑟 位上(位数从 0 开始),最低位的 1 在第 𝑙位,𝑡 的
lowbit等于1 << (r+1),𝑥 的lowbit等于1 << l,(((t&-t)/(x&-x))>>1)得到的,就是(1<<(r+1))/(1<<l)/2 = (1<<r)/(1<<l) = 1<<(r-l),在二进制表示下就是 1 后面跟上 𝑟 −𝑙 个零,零的个数正好等于连续 1 的个数减去 1 .举我们刚才的数为例,\(\frac{lowbit(t)/2}{lowbit(x)} =\frac{(00100)_2}{(00010)_2} =(00010)_2\) .把这个数减去 得到的就是我们要补全的低位,或上原来的数就可以得到答案.
所以枚举 0 ∼𝑛 按汉明权重递增的排列的完整代码为:
for (int i = 0; (1<<i)-1 <= n; i++) {
for (int x = (1<<i)-1, t; x <= n; t = x+(x&-x), x = x ? (t|((((t&-t)/(x&-x))>>1)-1)) : (n+1))
{
// 写下需要完成的操作 注意0的特判
}
}
二进制集合操作
一个数对 2 的非负整数次幂取模,等价于取二进制下一个数的后若干位,等价于和 𝑚𝑜𝑑 −1 进行与操作
子集遍历
// 降序遍历 m 的子集
for (int s = m;; s = (s - 1) & m) {
// s 是 m 的一个子集
if (s == 0) break;
}
遍历所有掩码的子掩码
复杂度为 \(O(3^n)\)
for (int m = 0; m < (1 << n); ++m)
// 降序遍历 m 的非空子集
for (int s = m; s; s = (s - 1) & m)
// s 是 m 的一个非空子集
区间位运算的经典性质
对于一个长为 \(n\),值域为 \(W\) 的序列,对于每个点 \(i\) ,以 \(i\) 为左端点的区间 and/or/gcd值只有 \(O(logW)\)个。
快速幂
ll qpow(ll x,ll y){
ll r=1;
for(;y;y>>=1){
if(y&1)r=r*x%mod;
x=x*x%mod;
}
return r;
}
ll fm(ll x){return qpow(x,mod-2);}
离散化
memcpy(b+1,a+1,sizeof(int)*n);
sort(b+1,b+n+1);
m=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;++i)a[i]=lower_bound(b+1,b+m+1,a[i])-b;
二分
整数二分
int l = ..., r = ..., mid, ans = r + 1;
while (l <= r) {
mid = (l + r) / 2;
if (check(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
实数二分
const double eps = 1e-6;
double l = ..., r = ..., mid, ans = r;
while (r - l > eps) { mid = (l + r) / 2;
if (check(mid)) ans = mid, r = mid;
else l = mid;
}
01分数规划
01 分数规划描述的是在一些物品中(在某种规则的限制下)选出一部分,最大化收益与代价的比值的问题。即最大化:
解决方法一般是二分答案 \(x\) ,判断是否有使比值 \(\geq x\) 的选择方法。
即:
三分
取三等分点 \(lmid,rmid\) 三分求一些单峰函数的极值点
bool ck2(pdb Lmid,pdb Rmid,pdb e)
{
db tmp1=calc1(e1,e2,Lmid.first,Lmid.second);
db tmp2=calc1(e1,e2,Rmid.first,Rmid.second);
return tmp1>tmp2;
}
db solve(pdb e)
{
pdb L=mp(c1,c2),R=mp(d1,d2);
pdb Lmid=mp((L1+(R1-L1)/3),(L2+(R2-L2)/3)),Rmid=mp((R1-(R1-L1)/3),(R2-(R2-L2)/3));
while(dis(L1,L2,R1,R2)>eps)//临界条件为两个点的接近程度
{
if(ck2(Lmid,Rmid,e))L=Lmid;
else R=Rmid;
Lmid=mp((L1+(R1-L1)/3),(L2+(R2-L2)/3)),Rmid=mp((R1-(R1-L1)/3),(R2-(R2-L2)/3));
}
return calc1(e1,e2,L1,L2)+calc2(e1,e2);
}
字符串哈希
\(f(i)=\Sigma{s[i]}\times base^{|s|-i} (mod MOD)\)
多次询问子串哈希
const int MOD=1e9+7;
const int base=19260817;
ll pw[N],hs[N];
void init(int n,char* s)
{ pw[0]=1;
for(int i=1;i<=n;++i){
pw[i]=pw[i-1]*base%MOD;
hs[i]=(hs[i-1]*base+s[i])%MOD;
}
}
inline ll mod(ll x){ return (x%MOD+MOD)%MOD; }
inline ll hash(int l,int r){ return mod(hs[r]-hs[l-1]*pw[r-l+1]); }
一些推导:
KMP
nex[0]=-1;
for(int i=1,j=-1;i<=m;++i){
while(j!=-1&&t[i]!=t[j+1])j=nex[j];
nex[i]=++j;
}
for(int i=1,j=0;i<=n;++i){
while(j!=-1&&s[i]!=t[j+1])j=nex[j];
++j;
if(j==m)
{
/*do something*/
j=nex[j];
}
}
//t in s
一些STL
set<int> s;
s.insert(3);
s.insert(1);
s.insert(4); s.insert(2);
cout<<*s.begin()<<'\n'; // 最小值 1
//set<int>::iterator it=s.end();
auto it=s.end(); --it; // 前一个元素
cout<<*it<<'\n'; // 最大值 4
s.erase(1); // 删除 1
cout<<*s.begin()<<'\n'; // 最小值变为 2
auto it=s.find(3); // 查找 3。返回的是 3 这个位置的迭代器
it=s.find(1); // 查找 1。1 已经不存在,所以返回的是 s.end()
cout<<*s.lower_bound(2)<<'\n'; // 查找大于等于 2 的最小元素,输出 2
cout<<*s.upper_bound(2)<<'\n'; // 查找大于 2 的最小元素,输出 3
priority_queue<int,vector<int>,greater<int>>q;
bool operator>(const node & b)const{return val>b.val;}
mid = first + k
std::partial_sort(first, mid, last);
std::partial_sort(first, mid, last, cmp);
struct data {
int a, b;
bool operator<(const data rhs) const {
return (a == rhs.a) ? (b < rhs.b) : (a < rhs.a);
}
} da[1009];
bool cmp(const data u1, const data u2) {
return (u1.a == u2.a) ? (u1.b > u2.b) : (u1.a > u2.a);
}
// ...
std::sort(da + 1, da + 1 + 10); // 使用结构体中定义的 < 运算符,从小到大排序
std::sort(da + 1, da + 1 + 10, cmp); // 使用 cmp 函数进行比较,从大到小排序
int N = 9, a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
do {
for (int i = 0; i < N; i++) cout << a[i] << " ";
cout << endl;
} while (next_permutation(a, a + N));
int N = 10, a[] = {1, 1, 2, 4, 5, 5, 7, 7, 9, 9}, x = 5;
int i = lower_bound(a, a + N, x) - a, j = upper_bound(a, a + N, x) - a;
// a[0] ~ a[i - 1] 为小于x的元素, a[i] ~ a[j - 1] 为等于x的元素,
// a[j] ~ a[N - 1] 为大于x的元素
cout << i << " " << j << endl;
//map的find没找到时返回m.end()
vector<vector<bool>>v;
v.resize(mx);
for(int i=0;i<mx;i++)v[i].resize(my,0);
a.erase(unique(a.begin(), a.end()), a.end());
vector<vector<bool>> v(mx, vector<bool>(my, false));
//int转string->stoi
//string转int->to_string
(X + Y - 1) / Y//向上取整X/Y
并查集
int f[N], s[N]; // 取秩为集合大小
inline void init(int n) { for(int i=1; i<=n; ++i)f[i]=i, s[i]=1; }
int find(int x) { return x==f[x]?x:f[x]=find(f[x]); } // 路径压缩
inline void merge(int x, int y){ // 按秩合并
x=find(x), y=find(y);
if(x==y)return;
if(s[x]>s[y])swap(x, y);
f[x]=y, s[y]+=s[x];
}
带权并查集
typedef std::pair<int, int> pii;
int f[N], s[N], d[N]; // 取秩为集合大小
inline void init(int n) { for(int i=1; i<=n; ++i)f[i]=i, s[i]=1, d[i]=0; }
pii find(int x) {
if(x==f[x])return pii(f[x],d[x]);
pii res=find(f[x]);
f[x]=res.first, d[x]+=res.second; // 路径压缩
res.second=d[x]; return res;
}
inline bool merge(int x, int y, int z){ // x --z-> y
pii px=find(x), py=find(y);
z+=py.second-px.second;
x=px.first, y=py.first;
if(x==y)return false;
if(s[x]>s[y])swap(x, y), z=-z; // 按秩合并
f[x]=y, s[y]+=s[x], d[x]=z;
}
ST表
lg[1]=0;
for(int i=2;i<=n;i++)lg[i]=lg[i>>1]+1;
for(int i=1;i<=n;i++)cin>>st[i][0];
for(int j=1;j<=lg[n];j++)
{
for(int i=1;i<=n-(1<<j)+1;i++)
{
st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
for(int i=1;i<=m;i++)
{
int l,r,k;
cin>>l>>r;
k=lg[r-l+1];
cout<<max(st[l][k],st[r-(1<<k)+1][k])<<'\n';
}
一/二维差分与前缀和
一维差分
a[r+1]--,a[l]++
二维差分
k[a][b]++;
k[c+1][b]--;
k[a][d+1]--;
k[c+1][d+1]++;
二维前缀和
k[i][j]=k[i-1][j]+k[i][j-1]-k[i-1][j-1]+k[i][j];
倍增求LCA
int fa[maxn][25],dep[maxn],lg[maxn];
int L;
void dfs(int u)
{
for(int i=1;i<=L;i++)fa[u][i]=fa[fa[u][i-1]][i-1];
for(int v:G[u])
{
if(v!=fa[u][0])
{
fa[v][0]=u;
dep[v]=dep[u]+1;
dfs(v);
}
}
}
int lca(int u,int v)
{
if(dep[u]<dep[v])swap(u,v);
int tmp=dep[u]-dep[v];
for(int i=L;i>=0;i--)if((tmp>>i)&1)u=fa[u][i];
if(u==v)return u;
for(int i=L;i>=0;i--)if(fa[u][i]^fa[v][i])u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
树上差分
点权
d[u]++,d[v]++,d[lca(u,v)]--,d[fa[lca(u,v)]]--;
边权
d[u]++,d[v]++,d[lca(u,v)]-=2;
树状数组
int a[maxn],c[maxn];
int lb(int x){return x&-x;}
int n,m;
void upd(int x,int p)
{
for(;x<=n;x+=lb(x))c[x]+=p;
}
int qry(int x)
{
int ans=0;
for(;x;x-=lb(x))ans+=c[x];
return ans;
}
线段树
#define Mid ((l+r)>>1) // 这些宏定义能极大程度地帮助你简化代码,同时降低写挂的可能性
#define Len (r-l+1)
#define Root 1,1,n
#define Lpos pos<<1
#define Rpos Lpos|1
#define Lson Lpos,l,Mid
#define Rson Rpos,Mid+1,r
#define This pos,l,r
int val[N],dat[N<<2],tag[N<<2];
inline void stg(int pos,int l,int r,int v){ // 添加标记
tag[pos]+=v,dat[pos]+=Len*v;
}
inline void psd(int pos,int l,int r){ // 下传标记
stg(Lson,tag[pos]),stg(Rson,tag[pos]),tag[pos]=0;
}
inline void upd(int pos){
dat[pos]=dat[Lpos]+dat[Rpos];
}
void bld(int pos,int l,int r){
if(l==r)dat[pos]=val[l];
else bld(Lson),bld(Rson),upd(pos);
}
void upd(int pos,int l,int r,int L,int R,int v){ // 区间修改
if(L<=l&&r<=R)return stg(This,v),void();
psd(This);
if(L<=Mid)upd(Lson,L,R,v);
if(R> Mid)upd(Rson,L,R,v);
upd(pos);
}
int qry(int pos,int l,int r,int L,int R){ // 区间查询
if(L<=l&&r<=R)return dat[pos];
psd(This);
if(R<=Mid)return qry(Lson,L,R);
if(L> Mid)return qry(Rson,L,R);
return qry(Lson,L,R)+qry(Rson,L,R);
}
struct node {
int sum, pre, suf, ans;
node() {}
node(int val) { sum = pre = suf = ans = val; }
inline node operator+(const node& r) const {
node res;
res.sum = sum + r.sum;
res.pre = max(pre, sum + r.pre);
res.suf = max(suf + r.sum, r.suf);
res.ans = max(max(ans, r.ans), suf + r.pre);
return res;
}
};
Trie
struct node{
int c[26];
int tag;
}a[N];
int rot,tot=0;
inline void init(){ rot=++tot; }
void insert(char* s){
int pos=rot,val;
for(;*s;++s){
val=(*s)-'a';
if(!a[pos].c[val])a[pos].c[val]=++tot; //不存在则新建子结点
pos=a[pos].c[val];
}
a[pos].tag++; //标记尾结点
}
int find(char* s){
int pos=rot,val;
for(;*s;++s){
val=(*s)-'a';
if(!a[pos].c[val])return -1; //未找到
pos=a[pos].c[val];
}
return a[pos].tag; //返回尾结点的标记
}
最小生成树
int n,m;
int fa[maxn];
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
bool cmp(node a,node b){return a.w<b.w;}
int ans;
void merge(int x,int y)
{
int fx=find(x),fy=find(y);
if(fx!=fy)
{
fa[fx]=fy;
}
}
int num=0;
void kr()
{
for(int i=1;i<=m;i++)
{
int fx=find(G[i].u),fy=find(G[i].v);
if(fx!=fy)
{
merge(G[i].u,G[i].v);
ans+=G[i].w;
num++;
}
if(num==n-1)break;
}
}
for(int i=1;i<=m;i++)
{
cin>>G[i].u>>G[i].v>>G[i].w;
}
for(int i=1;i<=n;i++)fa[i]=i;
sort(G+1,G+m+1,cmp);
kr();
Dijkstra
void di(int s)
{
priority_queue<pii,vector<pii>,greater<pii>>q;
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
q.push(mp(0,s));
while(!q.empty())
{
int u=q.top().second;
q.pop();
if(vis[u])continue;
vis[u]=1;
for(auto ed:G[u])
{
int v=ed.first,w=ed.second;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push(mp(dis[v],v));
}
}
}
}
SPFA判负环
bool spfa()
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(num,0,sizeof(num));
queue<int> q;
q.push(1);
vis[1]=1,dis[1]=0;
while(!q.empty())
{
int u=q.front();q.pop();
vis[u]=0;
for(auto ed:G[u])
{
int v=ed.first,w=ed.second;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
num[v]++;
if(num[v]>=n)return 0;
if(!vis[v])
{
q.push(v);vis[v]=1;
}
}
}
}
return 1;
}
欧拉回路找环
vector<pii>G[maxn];
void add(int u,int v,int i)
{
G[u].push_back(mp(v,i));
G[v].push_back(mp(u,i));
}
int n,m,d[maxn];
int st[maxm];
int vis[maxm],in[maxn],num,p[maxm],tot,cur[maxm],ve[maxm];
vector<int>ans[maxm];
void dfs(int u)
{
vis[u]=1;
for(int &i=cur[u];i<G[u].size();i++)//当前弧优化
{
int v=G[u][i].first,id=G[u][i].second;
if(ve[id])continue;
ve[id]=1;
dfs(v);
}
if(in[u])
{
num++;
ans[num].push_back(u);
p[num]++;
while(st[tot]!=u&&tot)
{
ans[num].push_back(st[tot]);
in[st[tot]]=0;
tot--;
p[num]++;
}
ans[num].push_back(u);//注意u还在栈内因为一个点可能被包含在两个环内
}
else st[++tot]=u,in[u]=1;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v,s,t;
cin>>u>>v>>s>>t;
if(s==t)continue;
add(u,v,i);
d[u]++;d[v]++;
}
int fg=0;
for(int i=1;i<=n;i++)if(d[i]%2)fg=1;
if(fg)
{
cout<<"NIE\n";
return 0;
}
for(int i=1;i<=n;i++)
{
if(!vis[i])dfs(i);
}
cout<<num<<'\n';
for(int i=1;i<=num;i++)
{
cout<<p[i];
for(auto v:ans[i])cout<<' '<<v;
cout<<'\n';
}
return 0;
}
无向图三元环计数
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ldb = long double;
using pii = pair<ll,ll>;
#define CI const int
#define mp make_pair
CI maxn=2e5+5;
vector<int>G[maxn];
int vis[maxn];
void add(int u,int v){G[u].push_back(v);}
int n,m,d[maxn],U[maxn],V[maxn],deg[maxn];
int ans;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>U[i]>>V[i];
u=U[i],v=V[i];
d[u]++;
d[v]++;
}
for(int i=1;i<=m;i++)
{
int u=U[i],v=V[i]
if(d[u]<d[v])add(u,v);
else if(d[u]==d[v]&&u<v)add(u,v);
else add(v,u);
}
for(int u=1;u<=n;u++)
{
for(auto v:G[u])vis[v]=u;
for(auto v:G[u])
{
for(auto w:G[v])
{
if(vis[w]==u)ans++;
}
}
}
cout<<ans;
return 0;
}
Tarjan求强连通分量
void tarjan(int u) {
dfn[u] = low[u] = ++cnt; stk[++top] = u;
for (auto v: e[u]) {
if (!dfn[v]) tarjan(v), low[u] = min(low[u], low[v]);
else if (!bel[v]) low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
int v;
do {
v = stk[top--], bel[v] = scc;
// do somthing with node v in scc
} while (v != u);
}
}
割点
void tarjan(int u, int pre = 0) { // pre 记录 u 的父亲,规定根的父亲为 0
dfn[u] = low[u] = ++tot;
for (int v: e[u]) {
if (v == pre) continue; // 跳过父亲
if (!dfn[v]) {
tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) ++cut[u];
} else low[u] = min(low[u], dfn[v]); // 返祖边
}
if (!pre) --cut[u]; // 根结点需额外 - 1
}
后缀数组SA
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ldb = long double;
using pii = pair<ll,ll>;
#define CI const int
#define mp make_pair
CI maxn=1e6+5;
string s;
int n,m;
int b[maxn],sa[maxn],x[maxn],y[maxn];
void SA(){
int num;
for(int i=0;i<n;i++)b[x[i]=s[i]]++;
for(int i=1;i<=m;i++)b[i]+=b[i-1];
for(int i=n-1;i>=0;i--)sa[--b[x[i]]]=i;
for(int k=1;k<n;k<<=1)
{
num=0;
for(int i=n-k;i<n;i++)y[num++]=i;
for(int i=0;i<n;i++)if(sa[i]>=k)y[num++]=sa[i]-k;
for(int i=0;i<=m;i++)b[i]=0;
for(int i=0;i<n;i++)b[x[y[i]]]++;
for(int i=1;i<=m;i++)b[i]+=b[i-1];
for(int i=n-1;i>=0;i--)sa[--b[x[y[i]]]]=y[i],y[i]=0;
swap(x,y);
num=0;
x[sa[0]]=0;
for(int i=1;i<n;i++)x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
if(num==n)break;
m=num;
}
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>s;
n=s.size();
m=128;
SA();
for(int i=0;i<n;i++)cout<<sa[i]+1<<' ';
return 0;
}
\(sa_i\) 表示第 \(i\) 大的是谁,\(rk_i\) 表示它排第几
\(LCP-->\) 最长公共前缀 ,下文的 \(LCP(i,j)\) 表示 \(sa_i\) 与 \(sa_j\) 的最长公共前后缀。
有
1. \(LCP(i,j)=LCP(j,i)\)
2. \(LCP(i,i)=len(i)=n-sa_i+1\)
3. \(LCP(i,k)=min (LCP(i,j),LCP(j,k) ) (i \leq j \leq k)\)
4. \(LCP(i,k)=min_{j=i+1}^{k}(LCP(j,j-1))\)
定义 \(height_i=LCP(i,i-1)\) ,\(height_1=0\)
引理:\(h_i=height_{rk[i]}\) \(h_i \geq h_{i-1}+1\)
void height()
{
int k=0;//k可以看做当前的h[i-1]
for (int i=0;i<n;++i) rk[sa[i]]=i;//这个在文章的开头就提到过
for (int i=0;i<n;++i)
{
if (rk[i]==0) continue;//height[0]肯定为0
if (k) k--;//h[i] >= h[i-1]-1,所以直接从h[i-1]-1开始枚举
int j=sa[rk[i]-1];//j是i相邻的一个后缀,求height
while (j+k<=n && i+k<=n && s[i+k]==s[j+k]) k++;//枚举它们的LCP
ht[rk[i]]=k;//k就是LCP的值
}
}
由3可知求LCP变为RMQ问题
不同子串个数=\(\frac{n(n+1)}{2}-\Sigma_{i=1}^{n-1}height_i\)
线性基
void ins(ll x)
{
for(int i=51;i>=0;i--)
{
if(!(x&(1ll<<i)))continue;
if(d[i])x^=d[i];
else {d[i]=x;break;};
}
}
K大异或和
vector<int>p;
void bld()
{
for(int i=0;i<=51;i++)
for(int j=i-1;j>=0;j--)if(d[i]&(1ll<<j))d[i]^=d[j];
for(int i=0;i<=51;i++)if(d[i])p.push_back(i);
}
int a[maxn],m,x,tot;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],ins(a[i]);
bld();
cin>>m;
tot=p.size();
for(int i=1;i<=m;i++)
{
cin>>x;
int res=0;
if(tot<n)x--;//有0
if(x>=(1ll<<tot)){cout<<"-1\n";continue;}
for(int j=0;j<tot;j++)if(x&(1ll<<j))res^=d[p[j]];
cout<<res<<'\n';
}
return 0;
}
背包DP
1.背包模板
01
for(int i=1;i<=n;i++)
for(int j=V;j>=v[i];j--)
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
完全
for(int i=1;i<=n;i++)
for(int j=v[i];j<=V;j++)
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
多重(强转01背包复杂度 \(O(nmk)\) )
1.二进制拆分+01(\(O( \sum_{i=1}^{n}\log(num[i])*V)\))
for(int i=1;i<=n;i++)
{
for(int j=1;j<=num[i];j<<=1)
//二进制每一位枚举.
//注意要从小到大拆分
{
num[i]-=j;//减去拆分出来的
new_c[++tot]=j*c[i];//合成一个大的物品的体积
new_w[tot]=j*w[i];//合成一个大的物品的价值
}
if(num[i])//判断是否会有余下的部分.
//就好像我们某一件物品为13,显然拆成二进制为1,2,4.
//我们余出来的部分为6,所以需要再来一份.
{
new_c[++tot]=num[i]*c[i];
new_w[tot]=num[i]*w[i];
num[i]=0;
}
}
2.单调队列优化( \(O(n*V)\) )
for(int i=1;i<=n;i++)//枚举物品种类
{
cin>>c[i]>>w[i]>>num[i];//c,w,num分别对应 体积,价值,个数
if(V/c[i] <num[i]) num[i]=V/c[i];//求lim
for(int mo=0;mo<c[i];mo++)//枚举余数
{
head=tail=0;//队列初始化
for(int k=0;k<=(V-mo)/c[i];k++)
{
int x=k;
int y=f[k*c[i]+mo]-k*w[i];
while(head<tail && que[head].pos<k-num)head++;//限制长度
while(head<tail && que[tail-1].value<=y)tail--;
que[tail].value=y,que[tail].pos=x;
tail++;
f[k*c[i]+mo]=que[head].value+k*w[i];
//加上k*w[i]的原因:
//我们的单调队列维护的是前i-1种的状态最大值.
//因此这里加上k*w[i].
}
}
}
混合
for(int i=1;i<=n;i++)
{
if(完全背包)
{
for(int j=c[i];j<=V;j++)
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
else if(01背包)
{
for(int j=V;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
else// (朴素の多重背包)
{
for(int j=V;j>=0;j--)
for(int k=1;k<=num[i];k++)
if(j-k*c[i]>=0)
f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
}
}
分组背包
for(int i=1;i<=k;i++)//枚举组别
for(int j=V;j>=0;j--)//枚举体积
for(now=belong[i])//枚举第i组的物品.
{
if(j-c[i]>=0)
f[i][j]=max(f[i-1][j],f[i-1][j-c[now]]+w[now]);
else
f[i][j]=f[i-1][j];
}
有依赖的背包(不保证完全一致)|
for(int i=1;i<=n;i++)//枚举主件.
{
memset(g,0,sizeof g);//做01背包要初始化.
for(now=belong[i])//枚举第i件物品的附件.
{
for(int j=V-1;j>=c[now];j--)//因为要先选择主件才能选择附件,所以我们从V-1开始.
{
g[j]=max(g[j],g[j-1]+w[now]);
}
}
g[V]=g[V-1]+w[i];
for(int j=V;j>=0;j--)
for(int k=1;k<=V;k++)//此时相当于"打包" ..
{
if(j-k>=0)
f[j]=max(f[j],f[j-k]+w[i]+g[k-1]);
}
}
或者直接转树上背包(
泛化物品:该类物品并没有固定的体积和价值,而是它的价值随着你分配给它的体积而变化
具体问题具体分析(
输出方案
for(int i=1;i<=n;i++)
{
for(int j=V;j>=c[i];j--)
{
if(f[j]<f[j-c[i]]+w[i])
{
f[j]=f[j-c[i]]+w[i];
g[i][j]=true;///选第i件物品
}
else g[i][j]=false;///不选第i件物品
}
}
匈牙利算法
附赠模板一份
bool match(int i)
{
for(int j=head[i];j;j=e[j].nxt)
{
int to=e[j].to;
if(!vis[to])
{
vis[to]=1;
if(!p[to]||match(p[to]))
{
p[to]=i;
return 1;
}
}
}
return 0;
}
int hg()
{
int ans=0;
for(int i=1;i<=n;i++)//upd 2023.10.20 修锅:n表示一边点数
{
memset(vis,0,sizeof(vis));
if(match(i)) ans++;
}
return ans;
}
匈牙利算法主要用来解决两个问题:求二分图的最大匹配数和最小点覆盖数。
所以这题一眼最大匹配(
建议自行学习原理
本题答案(其实没差多少完全一样)
#include<bits/stdc++.h>
using namespace std;
#define CI const int
CI maxn=105;
CI maxm=2505;
struct node{
int nxt,to;
}e[maxm*2];
int n,m,x,y;
int p[maxn],cnt,head[maxm];
bool vis[maxn];
void add(int u,int v)
{
cnt++;
e[cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
bool match(int i)
{
for(int j=head[i];j;j=e[j].nxt)
{
int to=e[j].to;
if(!vis[to])
{
vis[to]=1;
if(!p[to]||match(p[to]))
{
p[to]=i;
return 1;
}
}
}
return 0;
}
int hg()
{
int ans=0;
for(int i=1;i<=m;i++)
{
memset(vis,0,sizeof(vis));
if(match(i)) ans++;
}
return ans;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
while(cin>>x>>y)
{
add(x,y);
}
cout<<hg();
return 0;
}
染色法判断二分图
1.关于二分图的判断
定理:一张无向图是二分图:当且仅当图中不存在奇环
证明自己想
2.染色法 复杂度 $ O(n) $
我们规定1或2代表一个点属于两个集合。
首先我们任选一个点染色成1,把和它相连的所有点染色成2。
然后再把所有和染色成2的点相邻的点染色成1
在每一次染色点时首先要判断一下这个点是否被染色过,如果被染色过并且和上一个点颜色相同,则代表染色失败,该图不是二分图。
染色法分dfs和bfs,因为dfs需要递归判断,so我推荐写bfs,具体放在模板中
模板:
bool bfs(int st)//给个起点
{
color[st]=1;//color数组表示染色为1或2
q.push(st);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=e[i].nxt)
{
int to=e[i].to;
if(color[u]==color[to]) return 0;//染色相同,back
if(!color[to])
{
color[to]=color[u]%2+1;//让1染2,2染1
q.push(to);//入队
}
}
}
return 1;
}
主函数中:
for(int i=1;i<=n;i++)
{
if(!color[i])
if(!bfs(i))
{
//do sth
}
}
3.本题思路
当然可以使用跑的飞快的并查集
考虑冲突则不为二分图,则要判断二分图
考虑答案单调,考虑二分答案
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
typedef long long ll;
#define mid ((l+r)>>1)
struct node{
ll nxt,to,val;
}e[maxn*2];
ll cnt,head[maxn];
void add(ll u,ll v,ll w)
{
e[++cnt].to=v;
e[cnt].nxt=head[u];
e[cnt].val=w;
head[u]=cnt;
}
ll n,m;
ll color[maxn];
ll l,r,ans;
queue<ll> q;
void clq(){
queue<ll> ept;
swap(q,ept);
}
bool bfs(ll st,ll k)
{
q.push(st);
color[st]=1;
while(!q.empty())
{
ll u=q.front();q.pop();
for(int i=head[u];i;i=e[i].nxt)
{
ll to=e[i].to;
ll w=e[i].val;
if(w>=k)
{
if(color[to]==color[u]) return 0;
if(!color[to])
{
color[to]=color[u]%2+1;
q.push(to);
}
}
}
}
return 1;
}
bool ch(ll k)
{
clq();
memset(color,0,sizeof(color));
for(int i=1;i<=n;i++)
{
if(!color[i])
{
if(!bfs(i,k)) return 0;
}
}
return 1;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
ll u,v,w;
cin>>u>>v>>w;
add(u,v,w);add(v,u,w);
r=max(r,w);
}
r++;
while(l+1<r)
{
if(ch(mid)) r=mid;
else l=mid;
}
cout<<l;
return 0;
}
DSU ON TREE
前置芝士:树剖
树上启发式合并 (叫 dsu on tree 但是与并查集没啥关系),是一种 $O(n\log n) $ 级别的树上离线算法,爆踩某些难写的树套树和数量级 \(O(n \sqrt m)\) 的树上莫队,且原理简单也易于实现()。
我们所熟悉的启发式合并是将小的集合并到大的集合里,像并查集的按秩合并和和持久化并查集的按树高合并,这样让小集合成为大集合的“子树”,更加利于我们控制集合大小和找到父亲。
通常能用 dsu on tree 的题大多与子树有关且没有修改。
一句话题意:一棵有 \(n\) 个点的有根树,每个节点被染色,\(m\) 次询问 \(i\) 为根子树最多颜色的编号和。
显然我们有 \(O(n^2)\) 的暴力,从叶子到根依次用桶统计颜色个数和计算,那我们细看这个过程:统计以 \(u\) 为根的子树的答案时,会遍历每个 \(v \in son_u\),而我们先前刚已经统计了每个以 \(v\) 为根的答案并删除,考虑尽量的让统计次数变少,就需要保留刚刚 \(v\) 为根的贡献,保留哪个 \(son_u\) 最优呢?自然是子树大小最大的,即重儿子,这样整个统计就被分成了几步。
-
先像树剖一样预处理出重儿子
-
然后开始递归,对于所有非重儿子,暴力统计子树然后删除统计
-
再统计重儿子,这次统计不删除保留至父节点的统计中
-
按照如上顺序从叶子统计到根
时间复杂度 \(O(n \log n)\):
一个节点到根节点的路径不会超过 \(\log n\) 条:设根到该节点有 \(x\) 条轻边,该节点的大小为 \(y\),根据轻重边的定义,轻边所连向的点的大小不会成为该节点总大小的一半。每经过一条轻边,\(y\) 就除 2,所以 \(y< n/2^x\),\(n>2^x\),\(x<\log n\)。一个点被访问到只有暴力统计轻边的时候,次数 \(< \log n\),通过重边显然只有一次。
每次统计贡献(add,del)是 \(O(k)\) 的话总复杂度 \(O(nk\log n)\) 当然大部分时候 \(k=1\)。
具体也像莫队一样有大概的模板:
void add(int u,int f)
{
buk[c[u]]++;
if(buk[c[u]]>mx)mx=buk[c[u]],sum=c[u];
else if(buk[c[u]]==mx)sum+=c[u];
for(auto v:G[u])
{
if(v==f||v==p)continue;//注意别统计重儿子因为已经统计过了
add(v,u);
}
}
void del(int u,int f)
{
buk[c[u]]--;
for(auto v:G[u])
{
if(v==f)continue;//要清空的一定是轻儿子就全清空
del(v,u);
}
}
void dfs2(int u,int f,bool op)//op--是否清空
{
for(auto v:G[u])
{
if(v==f)continue;
if(v!=son[u])dfs2(v,u,0);//非重儿子要清空
}
if(son[u])dfs2(son[u],u,1),p=son[u];//重儿子不清空,p--u对应的重儿子不统计
add(u,f);ans[u]=sum;//统计答案
if(!op)del(u,f),mx=sum=0;//轻儿子全清空
}
Nim 游戏的变形
前置芝士:SG 函数
基础 Nim 游戏
游戏一开始有 \(n\) 堆石子,每堆石子的数量为 \(a_i\),双方轮流取石子,遵循如下规则:
- 每次可以在某一堆中拿走若干个,但不能不拿
- 拿完最后一个石子的人获胜
结论:当 \(a_1 \oplus a_2 \oplus a_3 \oplus \cdots\oplus a_{n-1} \oplus a_n=0\) 时,先手必败;否则,先手必胜。
证明:
\(SG(0)=0\) 为败态,即当 \(a_1 \oplus a_2 \oplus a_3 \oplus \cdots\oplus a_{n-1} \oplus a_n=0\) 时,先手必败;否则,先手必胜。
K-Nim 游戏
将只能选 \(1\) 堆石子改为至多选 \(k\) 堆石子。
结论:把每堆石子数目用二进制表示,\(f_i\) 表示所有堆第 \(i\) 位的和 \(\mod (k+1)\) 的值,当 \(\exists f_i \neq 0\) 时,先手必败,否则,先手必胜。
证明:证明
反 Nim 游戏
和最普通的 Nim 游戏相同,不过是取走最后一个石子的人输。
结论:先手必胜当且仅当
- 所有堆的石子数都为 \(1\) 且只有偶数堆
- 存在 \(a_i>1\) 的堆且 \(a_1 \oplus a_2 \oplus a_3 \oplus \cdots\oplus a_{n-1} \oplus a_n \neq 0\) 即 \(SG\) 值 \(\neq 0\)(与普通 Nim一样)
证明:情况一是显然的。
对于情况二,\(A:\) 当只有一堆石子个数大于 \(1\) 时,先手可以控制是余一个还是全拿走来控制情况一的产生,先手必胜。
\(B:\) 当不止一堆石子个数大于 \(1\) 时,转化为普通 Nim 游戏,当异或和不为 \(0\),一定有一种情况转为异或和为 \(0\),防止 \(A\) 出现在后手,先手必胜。
阶梯 Nim 游戏
有 \(n\) 个位置 \(1...n\),每个位置上有\(a_i\) 个石子。有两个人轮流操作。操作步骤是:挑选 \(1...n\) 中任一一个存在石子的位置 \(i\),将至少 \(1\) 个石子移动至 \(i−1\) 位置(也就是最后所有石子都堆在在 \(0\) 这个位置)。谁不能操作谁输。求先手必胜还是必败。
结论:先手必胜当且仅当所有奇数堆的石子异或起来不为 \(0\),这个游戏等价于只看奇数阶梯上石子的普通 Nim 游戏。
证明:如果大家都只动奇位置的石子,那么这等价于两人在玩 Nim 游戏。假设这个 Nim 游戏先手必胜,后手想打破局面,动了偶位置的石子,先手可以再把这些石子移动到偶位置上,等价于没动,先手必胜。若先手必败,同理,后手强迫先手玩 Nim 游戏,先手必败。
树上 Nim 游戏
把阶梯 Nim 搬到树上。
结论:先手必胜当且仅当所有深度为奇数堆的石子异或起来不为 \(0\)。
证明:同上。
Manacher
1.关于Manacher算法:
可以解决回文字符串类的问题,如最长回文串;
时间复杂度为优秀的\(O(n)\),证明在下面
2.Manacher算法的预处理
回文半径:对于一个回文串,有且仅有一个对称中心,在一个回文串内,任选一段区间 X ,一定存在关于"回文对称中心"对称的一个区间 Y ,且把这个区间 Y 叫做关于区间 X 的对称区间,回文半径即以对称中心到任意一边的距离长度。
Manacher算法就是预处理出每个位置的回文半径长记为\(d[i]\)
kfc定理:以i为中心回文半径长d等于以i为中心的奇回文子串的数量
证明:奇回文子串长分别为\(1,3,5...2d[i]-1\),数量显然相等
奇偶串分类:对于长度为奇数的字符串,比偶数字符串有一个更好的性质---有一个唯一的字符中心,而不是在两个字符之间,所以我们对字符串进行预处理,让字符串长度都变为奇数,即对\(len*2+1\),具体来说:
我们有这样一个字符串
aaa
123--下标
让它变为
@#a#a#a#
01234567--下标
其中#可以为任意不相干字符;至于为什么在0的位置上加入字符,待会说(
这样我们让奇回文子串变成以普通字符为中心的奇回文子串;原来的偶回文子串变成以#为中心的奇回文子串
最长回文串:在我们进行预处理后会发现回文串回文半径*2+1,恰好等于原回文串长+1,所以\(max_{i=1}^{n} d[i]-1\)即为最长回文串长。
代码:
s[0]='@';
s[1]='#';
for(int i=0;i<n;i++)
{
s[i*2+2]=a[i];
s[i*2+3]='#';
}
n=2*n+2;
s[n]=0;
或者更简便的
int init() {
int len = strlen(s);
str[0] = '@', str[1] = '#';
int j = 2;
for (int i = 0; i < len; ++i) str[j++] = s[i], str[j++] = '#';
str[j] = '\0';
return j;//j为扩展后串长
}
3.具体内容
这样我们的问题就转换成了怎样快速的求出\(d\)数组
然鹅当前主流求出\(d\)数组的方式有两种,分别为维护 \(l\) 和 \(r\) 以及维护 \(mid\) 和 \(r\)
(其中的 \(mid\) \(l\) \(r\) 分别代表最靠右的回文字串的中,左,右3点)
其实方法异曲同工,这里只提供第一种做法,第二种的代码会给出🐷
我们从左到右枚举 \(i\),其中 \(i≥l\)
然后大力分类讨论:
\(case 1: i>r\)
直接暴力计算 \(d[i]\),同时维护 \(l\) 和 \(r\).

if (i > r) {
while (i - d[i] >= 0 && i + d[i] < n && s[i - d[i]] == s[i + d[i]])
d[i]++;
l = i - d[i] + 1, r = i + d[i] - 1;
}
\(case 2: i≤r\)
找到 \(i\) 关于中心对称的 \(j\) ,\(j=l+r-i\);
证明:\((l+r)/2=(i+j)/2\) \(\Rightarrow\) \(i+j=l+r\) \(\Rightarrow\) \(j=l+r-i\)
此时若以 \(j\) 为中心的最长奇回文串的左边界大于 \(l\),令 \(d[i]=d[j]\),因为对称;

// int j = l + r - i;
else if (j - d[j] >= l) { // 即 j - d[j] + 1 > l
d[i] = d[j];
}
此时若以 \(j\) 为中心的最长奇回文串的左边界小于 \(l\),尽可能向左右拓展,同时更新 \(l\) 和 \(r\)

else {
d[i] = j - l + 1;
while (i - d[i] >= 0 && i + d[i] < n && s[i - d[i]] == s[i + d[i]])
//暴力拓展左右两侧,当t-p[t]==0时,由于s[0]是'@',自动停止。故不会下标溢出。
d[i]++;
l = i - d[i] + 1, r = i + d[i] - 1;
}
so,合起来
for (int i = 0, l = 0, r = -1; i < n; ++i) {
int j = l + r - i, dj = j >= 0 ? d[j] : 0;
d[i] = max(min(dj, j - l + 1), 0);
if (j - dj < l) {
while (i - d[i] >= 0 && i + d[i] < n && s[i - d[i]] == s[i + d[i]])
d[i]++;
l = i - d[i] + 1, r = i + d[i] - 1;
}
}
方法2:
int manacher() {
int ans = -1, len = init(), mx = 0, id = 0;
for (int i = 1; i < len; ++i) {
if (i < mx) d[i] = min(d[id * 2 - i], mx - i);
else d[i] = 1;
while (s[i + p[i]] == s[i - d[i]]) d[i]++;
if (d[i] + i > mx) mx = d[i] + i, id = i;
ans = max(ans, d[i] - 1);
}
return ans;
}
DFS序及其应用
树的DFS序列,也就是树的深搜序,它的概念是:树的每一个节点在深度优先遍历中进出栈的时间序列
vector<int> e[N];
int n;
int ln[N],rn[N],id[N],tot;
//ln[i]表示i的dfs序,rn[i]表示i的子树中最后访问到的节点dfs序。
//因此节点i的子树对应的区间为 [ln[i],rn[i]]。
//id[i]表示dfs序中i对应的原节点编号
void dfs(int u,int fa){
ln[u]=++tot;
id[tot]=u;
for(auto v:e[u]){
if(v==fa) continue;
dfs(v,u);
}
rn[u]=tot;
}
树剖
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
struct node{
ll nxt,to;
}e[maxn*2];
int cnt,head[maxn];
void add(int u,int v)
{
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
int dfn[maxn],top[maxn],fa[maxn],dep[maxn],son[maxn],rk[maxn],w[maxn],tot,siz[maxn];
void dfs1(ll u,ll f,ll d)
{
fa[u]=f;
dep[u]=d;
siz[u]=1;
ll ms=0;
for(int i=head[u];i;i=e[i].nxt)
{
int to=e[i].to;
if(to==f)continue;
dfs1(to,u,d+1);
siz[u]+=siz[to];
if(siz[to]>ms)ms=siz[to],son[u]=to;
}
}
void dfs2(ll u,ll t)
{
top[u]=t;
dfn[u]=++tot;
rk[tot]=w[u];
if(!son[u])return;
dfs2(son[u],t);
for(int i=head[u];i;i=e[i].nxt)
{
ll to=e[i].to;
if(to==fa[u]||to==son[u])continue;
dfs2(to,to);
}
}
#define mid ((l+r)>>1)
#define Lpos pos<<1
#define Rpos Lpos|1
#define Lson Lpos,l,mid
#define Rson Rpos,mid+1,r
#define rot 1,1,n
#define len (r-l+1)
#define This pos,l,r
ll mod,r,n,m;
ll ad(ll a,ll b){return (a%mod+b%mod+mod)%mod;}
ll ch(ll a,ll b){return a*b%mod;}
ll dat[maxn<<2],tag[maxn<<2];
void upd(ll pos){dat[pos]=ad(dat[Lpos],dat[Rpos]);}
void stg(ll pos,ll l,ll r,ll v)
{
dat[pos]=ad(dat[pos],ch(len,v));
tag[pos]=ad(tag[pos],v);
}
void psd(ll pos,ll l,ll r)
{
if(!tag[pos])return;
stg(Lson,tag[pos]);
stg(Rson,tag[pos]);
tag[pos]=0;
}
void bld(ll pos,ll l,ll r)
{
if(l==r)dat[pos]=rk[l];
else bld(Lson),bld(Rson),upd(pos);
}
void update(ll pos,ll l,ll r,ll L,ll R,ll v)
{
if(L<=l&&R>=r)return stg(This,v),void();
psd(This);
if(L<=mid)update(Lson,L,R,v);
if(R>mid)update(Rson,L,R,v);
upd(pos);
}
ll qry(ll pos,ll l,ll r,ll L,ll R)
{
if(L<=l&&R>=r)return dat[pos];
psd(This);
if(R<=mid)return qry(Lson,L,R);
if(L>mid)return qry(Rson,L,R);
return ad(qry(Lson,L,R),qry(Rson,L,R));
}
void upd_path(ll x,ll y,ll v)
{
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]])swap(x,y);
update(rot,dfn[top[x]],dfn[x],v);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
update(rot,dfn[x],dfn[y],v);
}
ll qry_path(ll x,ll y)
{
ll ans=0;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]])swap(x,y);
ans=ad(ans,qry(rot,dfn[top[x]],dfn[x]));
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
ans=ad(ans,qry(rot,dfn[x],dfn[y]));
return ans;
}
void upd_son(ll x,ll v){update(rot,dfn[x],dfn[x]+siz[x]-1,v);}
ll qry_son(ll x){return qry(rot,dfn[x],dfn[x]+siz[x]-1);}
ll op,x,y,z;
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m>>r>>mod;
for(int i=1;i<=n;i++)cin>>w[i],w[i]%=mod;
for(int i=1;i<n;i++)
{
ll u,v;
cin>>u>>v;
add(u,v),add(v,u);
}
dfs1(r,0,1),dfs2(r,r);
bld(rot);
while(m--)
{
cin>>op;
if(op==1)
{
cin>>x>>y>>z;
upd_path(x,y,z);
}
else if(op==2)
{
cin>>x>>y;
cout<<qry_path(x,y)<<'\n';
}
else if(op==3)
{
cin>>x>>y;
upd_son(x,y);
}
else
{
cin>>x;
cout<<qry_son(x)<<'\n';
}
}
return 0;
}
int val[N],dat[N<<2],tag[N<<2];
inline void stg(int pos,int l,int r,int v){ // 添加标记
tag[pos]+=v,dat[pos]+=Len*v;
}
inline void psd(int pos,int l,int r){ // 下传标记
stg(Lson,tag[pos]),stg(Rson,tag[pos]),tag[pos]=0;
}
inline void upd(int pos){
dat[pos]=dat[Lpos]+dat[Rpos];
}
void bld(int pos,int l,int r){
if(l==r)dat[pos]=val[l];
else bld(Lson),bld(Rson),upd(pos);
}
void upd(int pos,int l,int r,int L,int R,int v){ // 区间修改
if(L<=l&&r<=R)return stg(This,v),void();
psd(This);
if(L<=Mid)upd(Lson,L,R,v);
if(R> Mid)upd(Rson,L,R,v);
upd(pos);
}
int qry(int pos,int l,int r,int L,int R){ // 区间查询
if(L<=l&&r<=R)return dat[pos];
psd(This);
if(R<=Mid)return qry(Lson,L,R);
if(L> Mid)return qry(Rson,L,R);
return qry(Lson,L,R)+qry(Rson,L,R);
}
LIS
d[1] = a[1];
int len = 1;
for (int i = 2; i <= n; i++) {
if (a[i] > d[len])
d[++len] = a[i];
else {
int j = lower_bound(d + 1, d + len + 1, a[i]) - d;
d[j] = a[i];
}
}
cout << len;
笛卡尔树
对于给定排列,可以构建一棵 最大堆笛卡尔树:
-
每个节点对应一个元素,且满足 父节点的值大于子节点。
-
中序遍历得到原数组顺序。
在这样一棵树中,任意一条从根到叶子的路径,按原数组位置排序后,恰好构成一个单谷子序列:
- 若路径一直向左,则位置递减、值递减,得到递增子序列。
- 若路径一直向右,则位置递增、值递减,得到递减子序列。
- 若路径先左后右,则位置先减后增、值一直递减,得到先减后增的V形子序列。
因此,树的高度(最长路径上的节点数)就是最长单谷子序列的长度。
在笛卡尔树中,一个节点是峰当且仅当它既有左孩子又有右孩子(因为左孩子在它左边且小于它,右孩子在它右边且小于它)。
每次操作选择一个峰,并删除它的一个邻居(左或右孩子)。删除后,该孩子消失,树结构变化,但剩余节点仍构成一棵笛卡尔树(原树的子树)
-如果保留的节点构成一条路径,那么这些节点在原数组中的顺序(中序遍历)恰好形成单谷序列:路径先向左走(对应左边部分递增),再向右走(对应右边部分递减),或者全左(递增)、全右(递减)。因此该子序列是 cool 的。
任何更长的子序列若 cool,则必然包含分支,但分支会导致存在峰,无法通过操作保留(因为分支上的节点会相互成为峰,且删除一个后另一个仍可能为峰,最终无法同时保留)
// stk 维护笛卡尔树中节点对应到序列中的下标
for (int i = 1; i <= n; i++) {
int k = top; // top 表示操作前的栈顶,k 表示当前栈顶
while (k > 0 && w[stk[k]] > w[i]) k--; // 维护右链上的节点
if (k) rs[stk[k]] = i; // 栈顶元素.右儿子 := 当前元素
if (k < top) ls[i] = stk[k + 1]; // 当前元素.左儿子 := 上一个被弹出的元素
stk[++k] = i; // 当前元素入栈
top = k;
}
// 建小根堆笛卡尔树
stack<int> st;
for (int i = 1; i <= n; i++) {
int last = 0;
while (!st.empty() && a[st.top()] > a[i]) {
last = st.top();
st.pop();
}
if (!st.empty()) {
right[st.top()] = i;
parent[i] = st.top();
}
left[i] = last;
if (last) parent[last] = i;
st.push(i);
}
int root = 1;
while (parent[root]) root = parent[root]; // 找根
// 迭代后序遍历
vector<int> post;
stack<int> stk;
stk.push(root);
while (!stk.empty()) {
int u = stk.top(); stk.pop();
post.push_back(u);
if (left[u]) stk.push(left[u]);
if (right[u]) stk.push(right[u]);
}
reverse(post.begin(), post.end()); // 现在为左-右-根的后序
计算几何
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ldb = long double;
using pii = pair<ll,ll>;
#define CI const int
#define mp make_pair
const db eps=1e-8;
const db PI=acos(-1.0);
CI maxn=1e4+5;
int sgn(db x) { return x < -eps ? -1 : x > eps; }
typedef struct Point{
db x,y;
Point(db x=0,db y=0):x(x),y(y){}
Point operator-(const Point&B)const {return Point(x-B.x,y-B.y);}
Point operator+(const Point&B)const {return Point(x+B.x,y+B.y);}
Point operator*(const db &B)const {return Point(x*B, y*B);}
db operator*(const Point&B)const {return x*B.x+y*B.y;}
db operator^(const Point&B)const {return x*B.y-B.x*y;}
Point operator/(const db& B)const {return Point(x/B,y/B);}
bool operator==(const Point &B)const {return sgn(x-B.x)==0&&sgn(y-B.y)==0;}
bool operator!= (const Point &B) const { return sgn(x - B.x) || sgn(y - B.y); }
bool operator< (const Point &B) const { return x < B.x || (x == B.x && y < B.y); }
}Vector;
db len(Vector A){return sqrt(A*A);}
Vector norm(Vector A){return A/len(A);}
double dist(Point a, Point b) { return sqrt((a - b) * (a - b)); }
Point Projection(Point A,Point B,Point p)
{
db k=(((p-A)*(B-A))/len(B-A));
Point o=A+(norm(B-A)*k);
return o;
}
Point Reflection(Point A,Point B,Point p)
{
Point X;
return Projection(A,B,p)*2.0-p;
}
int Cross(Point A,Point B,Point p){return sgn((B-A)^(p-A));}
// 点在直线上, 返回 0 (三点共线)
// 点在直线的逆时针方向, 返回 1
// 点在直线的顺时针方向, 返回 -1
void CounterClockwise(Point A,Point B,Point p){
int ck=Cross(A,B,p);
if(ck==-1)cout<<"CLOCKWISE\n";
else if(ck==1)cout<<"COUNTER_CLOCKWISE\n";
else
{
if(sgn((B-A)*(p-A))==-1)cout<<"ONLINE_BACK\n";
else if(sgn((A-B)*(p-B))==-1)cout<<"ONLINE_FRONT\n";
else cout<<"ON_SEGMENT\n";
}
}
bool OnSegment(Point A,Point B,Point P) {
Vector PA=A-P,PB=B-P;
return sgn(PA^PB)==0&&sgn(PA*PB)<=0; // <=, 包括端点; <, 不包括端点
}
bool seg_seg(Point a,Point b,Point c,Point d){
if(OnSegment(c,d,a)||OnSegment(c,d,b)||OnSegment(a,b,c)||OnSegment(a,b,d))return 1;
if(Cross(a,b,c)*Cross(a,b,d)>=0)return 0;
if(Cross(c,d,a)*Cross(c,d,b)>=0)return 0;
return 1;
// 相交, 返回 true
// 不相交, 返回 false
}
// 首先要判断两直线是否相交, 即不平行(不重合)
// a, b 所在直线与 c, d 所在直线的交点
Point Intersection_line(Point a,Point b,Point c,Point d) {
Vector u=b-a,v=d-c;
db t=((a-c)^v)/(v^u);
return a+u*t;
}
Point Intersection_line_vector(Point a, Vector u, Point b, Vector v) {
db t =((a-b)^v)/(v^u);
return a+u*t;
}
db Point_to_line(Point A,Point B,Point p)
{
Vector v1=B-A,v2=p-A;
return fabs((v1^v2)/len(v1));
}
db Point_to_seg(Point A,Point B,Point p)
{
if(A==B)return len(p-A);
Vector v1=B-A,v2=p-A,v3=p-B;
if(sgn(v1*v2)<0)return len(v2);
if(sgn(v1*v3)>0)return len(v3);
return fabs((v1^v2)/len(v1));
}
db seg_to_seg(Point p0,Point p1,Point p2,Point p3)
{
if(seg_seg(p0,p1,p2,p3))return 0;
else
{
db ans=1e5,a,b,c,d;
a=Point_to_seg(p0,p1,p2);
b=Point_to_seg(p0,p1,p3);
c=Point_to_seg(p2,p3,p0);
d=Point_to_seg(p2,p3,p1);
ans=min(a,min(b,min(c,d)));
return ans;
}
}
// 向量 A 和要逆时针转的角度 [0, PI]
// PI / 2, 90度
Vector Rotate(Vector A, double b) {
Vector B(sin(b), cos(b));
return Vector(A ^ B, A * B);
}
db Angle(Vector A, Vector B) {
double t = acos((A * B) / len(A) / len(B));
return t; // 返回 [0, π]
return t * (180 / PI); // 返回 [0, 180] (角度)
}
db Polygon_area(Point *p, int n) {
double area = 0;
for (int i = 0, j = n - 1; i < n; j = i++)
area += (p[j] ^ p[i]);
return fabs(area / 2); // 无向面积
return area / 2; // 有向面积
}
int n;
Point p[maxn];
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
for(int i=0;i<n;i++)cin>>p[i].x>>p[i].y;
cout<<fixed<<setprecision(1)<<Polygon_area(p,n)<<'\n';
return 0;
}
欧拉回路找环
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ldb = long double;
using pii = pair<ll,ll>;
#define CI const int
#define mp make_pair
CI maxm=2e6+5;
CI maxn=1e5+5;
vector<pii>G[maxn];
void add(int u,int v,int i)
{
G[u].push_back(mp(v,i));
G[v].push_back(mp(u,i));
}
int n,m,d[maxn];
int st[maxm];
int vis[maxm],in[maxn],num,p[maxm],tot,cur[maxm],ve[maxm];
vector<int>ans[maxm];
void dfs(int u)
{
vis[u]=1;
for(int &i=cur[u];i<G[u].size();i++)//当前弧优化
{
int v=G[u][i].first,id=G[u][i].second;
if(ve[id])continue;
ve[id]=1;
dfs(v);
}
if(in[u])
{
num++;
ans[num].push_back(u);
p[num]++;
while(st[tot]!=u&&tot)
{
ans[num].push_back(st[tot]);
in[st[tot]]=0;
tot--;
p[num]++;
}
ans[num].push_back(u);//注意u还在栈内因为一个点可能被包含在两个环内
}
else st[++tot]=u,in[u]=1;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v,s,t;
cin>>u>>v>>s>>t;
if(s==t)continue;
add(u,v,i);
d[u]++;d[v]++;
}
int fg=0;
for(int i=1;i<=n;i++)if(d[i]%2)fg=1;
if(fg)
{
cout<<"NIE\n";
return 0;
}
for(int i=1;i<=n;i++)
{
if(!vis[i])dfs(i);
}
cout<<num<<'\n';
for(int i=1;i<=num;i++)
{
cout<<p[i];
for(auto v:ans[i])cout<<' '<<v;
cout<<'\n';
}
return 0;
}
哈希
const int MOD=1e9+7;
const int base=19260817;
ll pw[N],hs[N];
void init(int n,char* s){
pw[0]=1;
for(int i=1;i<=n;++i){
pw[i]=pw[i-1]*base%MOD;
hs[i]=(hs[i-1]*base+s[i])%MOD;
}
}
inline ll mod(ll x){ return (x%MOD+MOD)%MOD; }
inline ll hash(int l,int r){
return mod(hs[r]-hs[l-1]*pw[r-l+1]);
}
SOSDP
for(int i=0;i<w;++i)//依次枚举每个维度`
{
for(int j=0;j<(1<<w);++j)//求每个维度的前缀和`
{
if(j&(1<<i))s[j]+=s[j^(1<<i)];
}
}
for (int m = 0; m < (1 << n); ++m)//O(3^n)
// 降序遍历 m 的非空子集
for (int s = m; s; s = (s - 1) & m)
// s 是 m 的一个非空子集
ODT
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ldb = long double;
using pii = pair<int, int>;
#define CI const int
#define mp make_pair
struct node{
int l,r;
mutable int col;
node(ll _l,ll _r=-1,ll _val=0) : l(_l),r(_r),col(_val){}
bool operator<(const node&o)const{return l<o.l;}
};
set<node>odt;
int n,m,q,T;
vector<int>a;
vector<ll>ans;
auto split(int pos)
{
auto it=odt.lower_bound(node(pos));
if(it!=odt.end()&&it->l==pos)return it;
it--;
ll L=it->l,R=it->r,C=it->col;
odt.erase(it);
odt.insert(node{L,pos-1,C});
return odt.insert(node{pos,R,C}).first;
}
void assign(int l,int r,int c) {
auto itr=split(r+1),itl=split(l);
odt.erase(itl,itr);
odt.insert(node{l,r,c});
}
void add(int l, int r, int x) {
auto itr=split(r+1),itl=split(l);
for (auto it=itl;it!=itr;++it) {
if (it->col!=0){
ans[it->col]+=1ll*(it->r-it->l+1)*x;
}
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin>>T;
while(T--)
{
cin>>n>>m>>q;
a.assign(n+2,0);
ans.assign(m+2,0);
for(int i=1;i<=n;i++)cin>>a[i];
odt.clear();
for(int i=1;i<=n;)
{
int j=i;
while(j<=n&&a[j]==a[i])++j;
odt.insert(node{i,j-1,a[i]});
i=j;
}
while(q--)
{
int op,l,r,x;
cin>>op>>l>>r>>x;
if(op==1)assign(l,r,x);
else add(l,r,x);
}
for(int i=1;i<=m;i++)cout<<ans[i]<<' ';
cout<<'\n';
}
return 0;
}
ll kth(ll l,ll r,ll w)
{
ct rb=split(r+1);
vector<pair<ll,ll> > stk;
for (ct i=split(l);i!=rb;i++)//遍历区间内所有颜色段并扔进vector里面
stk.push_back(make_pair(i->val,i->ri-i->li+1));
sort(stk.begin(),stk.end());//排序
for (ll i=0;i<(ll)stk.size();i++)
{
w-=stk[i].second;
if (w<=0) return stk[i].first;
}
return 0;//不知道为什么不写这个就会报错,所以只好加上了。
}
ll quick_pow(ll w,ll h,ll p)//快速幂
{
ll res=1;w%=p;
while (h)
{
if (h&1) res*=w,res%=p;
w=w*w%p;
h>>=1;
}
return res;
}
ll getsum(ll l,ll r,ll w,ll h)//左右边界与指数和模数
{
ct rb=split(r+1);
ll res=0;
for (ct i=split(l);i!=rb;i++)
{
ll u1=(i->ri-i->li+1),u2=quick_pow(i->val,w,h);
res+=(ll)u1*u2;res%=h;//统计和
}
return res;
}
广义矩阵乘法
struct mat{
int a[4][4];
mat(){
for(int i=1;i<=3;i++)
for(int j=1;j<=3;j++)a[i][j]=inf;
}
void print(){
for(int i=1;i<=3;i++)
for(int j=1;j<=3;j++)
cout<<a[i][j]<<" \n"[j==3];
}
const int* operator[](int x)const{return a[x];}
int* operator[](int x){return a[x];}
friend mat operator*(const mat&b,const mat&c){
mat res;
for(int i=1;i<=3;i++)
for(int j=1;j<=3;j++)
for(int k=1;k<=3;k++)
res[i][j]=max(res[i][j],b[i][k]+c[k][j]);
return res;
}
};
树的同构
树哈希是一种快速判断树同构的算法,每一个结构不同的的树都有自己独一无二(尽量)的哈希值,与序号无关。
对于有根树来说,树的哈希值如下:
通常取 \(c=1\) ,\(base\) 是大质数(998244353)
整数之间 \(shift()\) 实现如下:
ull shift(ull x)
{
x^=base;
x^=x<<13;
x^=x>>7;
x^=x<<17;
return x^base;
}
\(shift\) 可以换成其他多项式哈希,据说 \(shift\) 好写还不被卡。
貌似这样写难以被卡掉
P4323 [JSOI2016] 独特的树叶
对于无根树:
常与换根dp相联系,求出有根树的哈希值是 \(O(n)\) 的,通过换根dp可以再 \(O(n)\) 的求出所有点为根的哈希值。
void get_hs(int u,int f,int id)//以u为根无根树
{
hs[u][id]=1;
for(auto v:G[u][id])
{
if(f==v)continue;
get_hs(v,u,id);
hs[u][id]+=shift(hs[v][id]);
}
}
void dfs(int u,int f,int id)
{
if(!id) mp1[shift(dp[u][0])]=1;
else if(G[u][id].size()==1&&mp1[dp[u][id]-1])ans=min(ans,u);//这里是因为题意
for(auto v:G[u][id])
{
if(v==f)continue;
ull tmp=dp[u][id]-shift(hs[v][id]);
dp[v][id]=hs[v][id]+shift(tmp);
dfs(v,u,id);
}
}
换根的简易证明:
\(rt\) 为原根,\(v_{1}\in son[rt]\) 为新根。
令原树 \(hs_{u}=c+\sum_{v\in son[u]}shift(hs_{v})\)
那么新树根
写法同上
tips:一定要用 newhs 来减!!!
树同构的哈希做法
简易思路:每个点为根跑一遍哈希,存起来比较,用 map 优化一个 n,复杂度 \(O(n^2m)\)
还有找树的重心优化复杂度的方法,但是数据小,不用也能过
缩点
vector<int> G[maxn];
vector<int> e[maxn];
int w[maxn],u[maxn],v[maxn],deg[maxn];
int dfn[maxn],st[maxn],top,tot,low[maxn],bel[maxn],scc;
int sum[maxn];
void tar(int u)
{
dfn[u]=low[u]=++tot;
st[++top]=u;
for(auto v:G[u])
{
if(!dfn[v])
{
tar(v);
low[u]=min(low[u],low[v]);
}
else if(!bel[v])low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
int z;
++scc;
do{
z=st[top--];
bel[z]=scc;
sum[scc]+=w[z];
}while(u!=z);
}
}
int dis[maxn];
void topu()
{
queue<int> q;
for(int i=1;i<=scc;i++)if(!deg[i])q.push(i),dis[i]=sum[i];
while(!q.empty())
{
int u=q.front();q.pop();
for(auto v:e[u])
{
dis[v]=max(dis[v],dis[u]+sum[v]);
--deg[v];
if(!deg[v])q.push(v);
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>w[i];
for(int i=1;i<=m;i++)
{
cin>>u[i]>>v[i];
G[u[i]].push_back(v[i]);
}
for(int i=1;i<=n;i++)if(!dfn[i])tar(i);
for(int i=1;i<=m;i++)
{
int u1=u[i],v1=v[i];
if(bel[u1]!=bel[v1])
{
e[bel[u1]].push_back(bel[v1]);
deg[bel[v1]]++;
}
}
topu();
int ans=0;
for(int i=1;i<=scc;i++)ans=max(ans,dis[i]);
cout<<ans;
return 0;
}
格雷码
格雷码是一个二进制数字系统,其中两个相邻数的二进制位只有一位不同.
二进制转格雷码
int g(int n) { return n ^ (n >> 1); }
格雷码转二进制
int rev_g(int g) {
int n = 0;
for (; g; g >>= 1) n ^= g;
return n;
}
实际应用
\(k\) 位二进制数的格雷码序列可以当作 \(k\) 维空间中的一个超立方体(二维里的正方形,一维里的单位向量)顶点的哈密尔顿回路,其中格雷码的每一位代表一个维度的坐标.
格雷码可以用来解决汉诺塔的问题.
置换环
求解数组排序元素间所需最小交换次数这类问题。
置换环将每个元素指向其排序后应在的位置,最终首位相连形成一个环(若数字在最终位置,则其自身成环),可知元素之间的交换只会在同一个环内进行,而每个环内的最小交换次数为环上元素个数。
则总交换次数= \(\sum(c_i-1)\) =数组长度-环的个数 (\(c_i\) 为每一个环的长度)
树上背包
#include <bits/stdc++.h>
using namespace std;
#define int ll
using ll = long long;
using ull = unsigned long long;
using db = double;
using ldb = long double;
using pii = pair<int, int>;
#define CI const int
#define mp make_pair
CI maxn=2e3+5; // n 通常 ≤ 2000
int n,m,sz[maxn],dp[maxn][maxn];
vector<int>G[maxn];
// dp[u][j]: 在 u 子树中选 j 个点的最大收益
void dfs(int u,int f)
{
sz[u]=1;
// 初始化: 选 u 自己
// dp[u][1]=val[u];
for(int v:G[u]){
if(v==f)continue;
dfs(v,u);
// 倒序合并, 保证每个子树只被用一次 (0/1 背包)
for(int i=sz[u];i>=1;--i) // 倒序, 保证每个子树只选一次
for(int j=1;j<=sz[v]&&i+j<=m;++j)
dp[u][i+j]=max(dp[u][i+j],dp[u][i]+dp[v][j]);
sz[u]+=sz[v];
}
}
void solve()
{
cin>>n>>m;
for(int i=1;i<=n;i++)G[i].clear(),fill(dp[i],dp[i]+m+1,-1e18);
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
G[u].push_back(v),G[v].push_back(u);
}
dfs(1,0);
cout<<dp[1][m]<<'\n';
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int T=1;
while(T--)solve();
return 0;
}
数论
快速幂/矩阵快速幂
略
线性逆元
inv[1] = 1;
for(int i = 2; i <= n; ++i) inv[i] = inv[mod % i] * (mod - mod / i) % mod;
exgcd
void exgcd(int a, int b, int& x, int& y) {
if(!b) { x = 1, y = 0; return; }
exgcd(b, a % b, y, x);
y -= x * (a / b);
}
求解 \(ax+by=c\) 只需求解 \(ax+by=gcd(a,b)=d\) 结果乘 $ \frac{c}{d}$ 即可
\(ax+by=gcd(a,b)=gcd(b,a \mod b)=bx'+(a-\lfloor \frac{a}{b}\rfloor b)y'=ay'+b(x'-\lfloor \frac{a}{b}\rfloor y')\)
裴蜀定理
$ \forall a,b,x,y$
且 \(\exist x,y\) 使 \(gcd(a,b)=ax+by\)
推论:若 \(gcd(a,b)=1\)
$ \exist x,y $ 使 \(ax+by=1\)
可推广到多个整数情况
Sylvester 定理
$ gcd(a_1,a_2)=1 $ 时,最大的不能被 \(xa_1+ya_2\) 表示的数为 \(a_1a_2-a_1-a_2\)
素数与筛法
质因数分解
for(int i = 2; i * i <= n; ++i) if(!(n % i)) {
while(!(n % i)) n /= i, ++cnt;
// do something for (i, cnt)
}
if(n > 1) // do something for (n, 1)
Eratosthenes 筛法
bool np[N];
void sieve(int n) {
for(int i = 2; i * i <= n; ++i) if(!np[i])
for(int j = i * i; j <= n; j += i) np[j] = 1;
}
欧拉筛
bool np[N];
int pri[N], cnt = 0;
void sieve(int n) {
for(int i = 2; i <= n; ++i) {
if(!np[i]) pri[++cnt] = i;
for(int j = 1; j <= cnt; ++j) {
np[i * pri[j]] = 1;
if(!(i % pri[j])) break;
}
}
}
可并堆(左偏树)
#define ls(x) tr[x].ls
#define rs(x) tr[x].rs
#define fa(x) tr[x].fa
#define val(x) tr[x].val
#define dst(x) tr[x].dist
#define id(x) tr[x].id
int find(int x){return fa(x)==x?x:fa(x)=find(fa(x));}//fa是指向根节点的指针
//记得初始化
int merge(int x,int y){
if(!x||!y)return x|y;
if(val(x)<val(y))swap(x,y);//大顶堆
rs(x)=merge(rs(x),y);
if(dst(ls(x))<dst(rs(x)))swap(ls(x),rs(x));
dst(x)=dst(rs(x))+1;
fa(ls(x))=fa(rs(x))=fa(x)=x;
return x;
}
void pop(int x){//x是编号
d(x)=-1;
fa(ls(x))=ls(x),fa(rs(x))=rs(x);
fa(x)=merge(ls(x),rs(x));
}
void new(){
++tot;
val(tot)=val[v];
id(tot)=v;
dst(tot)=1;
merge(find(u),tot);
}

浙公网安备 33010602011771号