模板合集

快读快写

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 分数规划描述的是在一些物品中(在某种规则的限制下)选出一部分,最大化收益与代价的比值的问题。即最大化:

\[ \frac{\Sigma{value_i}}{\Sigma{cost_i}} \]

解决方法一般是二分答案 \(x\) ,判断是否有使比值 \(\geq x\) 的选择方法。

即:

\[\Sigma{v}-x\times\Sigma{c}\geq0 \]

三分

取三等分点 \(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]); }

一些推导:

\[\begin{align} 令hs[x]=\Sigma_{i=1}^x{s[i]}*base^{x-i} \end{align} \]

\[\begin{align} hash(l,r)&=f(s[l...r])\\ &=\Sigma_{i=1}^{r-l+1}{s[l+i-1]}*base^{r-l+1-i}\\ &=\Sigma_{j=l}^r{s[j]}*base^{r-i}\\ &=\Sigma_{i=1}^{r}s[i]*base^{r-i}-\Sigma_{i=1}^{l-1}s[i]*base^{r-i}\\ &=hs[r]-base^{r-l+1}*\Sigma_{i=1}^{l-1}s[i]*base^{l-i-1}\\ &=hs[r]-base^{r-l+1}*hs[l-1] \end{align} \]

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\) 时,先手必败;否则,先手必胜。

证明:

\[\begin{align} &SG(0)=0 \\ &SG(x)=mex({SG(y)|x \rightarrow y})=x \\ SG 定理:&SG(a_1,a_2,\cdots,a_{n-1},a_{n})\\ &=SG(a_1)\oplus SG(a_2)\oplus \cdots\oplus SG(n)\\ &=a_1 \oplus a_2 \oplus a_3 \oplus \cdots\oplus a_{n}\\ \end{align} \]

\(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\).

%%%fr

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]\),因为对称;

%%%sjb

// int j = l + r - i;
else if (j - d[j] >= l) { // 即 j - d[j] + 1 > l 
    d[i] = d[j];
}

此时若以 \(j\) 为中心的最长奇回文串的左边界小于 \(l\),尽可能向左右拓展,同时更新 \(l\)\(r\)

%%%sjb

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;
    }
};

树的同构

树哈希是一种快速判断树同构的算法,每一个结构不同的的树都有自己独一无二(尽量)的哈希值,与序号无关。

对于有根树来说,树的哈希值如下:

\[ hash_{u}=(c+\sum_{v\in son[u]}shift(hash_{v}))\mod base \]

通常取 \(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})\)

那么新树根

\[\begin{align} newhs_{v1}&=c+\sum_{v\in son[v1]}shift(newhs_v)\\ newhs_{v1}&=c+\sum_{gs\in son[v1]} shift(gs)+shift(newhs_{rt}-shift(hs_{v1}))\\ newhs_{v1}&=hs_{v1}+shift(newhs_{rt}-shift(hs_{v1})) \end{align} \]

写法同上

tips:一定要用 newhs 来减!!!

树同构的哈希做法

P5043 【模板】树同构([BJOI2015]树的同构)

简易思路:每个点为根跑一遍哈希,存起来比较,用 map 优化一个 n,复杂度 \(O(n^2m)\)

AC代码

还有找树的重心优化复杂度的方法,但是数据小,不用也能过

缩点

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')\)

\[\begin{cases} x=y'\\ y=x'-\lfloor \frac{a}{b}\rfloor y'\\ \end{cases} \]

裴蜀定理

$ \forall a,b,x,y$

\[ gcd(a,b)|ax+by \]

\(\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);
}
posted @ 2026-05-26 15:04  luobo215  阅读(17)  评论(0)    收藏  举报