黑科技

前言

这里记录&总结一些平常 \(OI\) 竞赛中并不是很常用的毒瘤玩意。

黑科技

黑科技数据结构

1. 李超线段树

一种线段树维护某一类信息的方法。

支持区间加入一次函数,询问区间的一次函数最值。

修改复杂度为 \(O(log^2n)\) , 如果是全局加入就是\(O(log\ n)\)
查询复杂度为 \(O(log\ n)\)

维护的信息:

flag[u]   当前节点是否插入了一次函数
K[u],B[u]  表示当前节点区域中最优的一次函数
Min/Max[u] 区间的最小值或最大值,一般是在询问的是区间的时候才有必要维护 

李超线段树维护信息的方法就是线段树的标记永久化 , 似乎不能支持删除。

对于一个区间修改 , 如果当前区间没有加入过直线 , 那么直接加入。
如果已有直线,比较两条直线,如果一条完全优于另外一条 , 那么替换或者是直接返回。
如果各有优劣,则看哪一条在当前区间内优势的部分较多,保留多的那一个,劣的继续往下递归。
由于一次函数的单调性 , 如果我们保留长的在当前区间 , 那么往下递归插入的一次函数只用插向一边,因为只会存在一个优劣转换的点,这样长度每次减半,复杂度就是 \(O(log\ n)\)
如果是区间插入 , 那么要先定位到 \(O(log\ n)\) 个区间,复杂度就是 \(O(log^2n)\) 了。
对于比较那个点的优势区间更长,可以采取算交点的方式,但我一般采用比较中点函数值的大小来推算,稍加讨论就行了。

\(Tips:\)
如果插入的一次函数并不是平常的连续一次函数 , 而是有实际意义的 , 那么一定要搞清楚到底 \(x\) 是什么 , 选好合适的 \(x\) 的含义来简化计算!

区间插入的模板:

void Modify(int u,int l,int r,int L,int R,ll k,ll b){
    int mid=l+r>>1;
    if(l>=L&&r<=R){
        if(!T[u].flag) {T[u].Fill(k,b,l,r);return;}
        ll yln=F(k,b,l),yrn=F(k,b,r),ylo=T[u].F(l),yro=T[u].F(r);
        if(yln>=ylo&&yrn>=yro) return;
        if(yln<=ylo&&yrn<=yro) return T[u].Fill(k,b,l,r);
        else {
            ll ymn=F(k,b,mid),ymo=T[u].F(mid);
            if(ymn>=ymo) {
                if(yln>=ylo) Modify(rs,mid+1,r,L,R,k,b);
                else Modify(ls,l,mid,L,R,k,b);
            }
            else {
                if(yln>=ylo) Modify(ls,l,mid,L,R,T[u].k,T[u].b),T[u].Fill(k,b,l,r);
                else Modify(rs,mid+1,r,L,R,T[u].k,T[u].b),T[u].Fill(k,b,l,r);
            }
            T[u].Min=min(T[u].Min,min(T[ls].Min,T[rs].Min));
            return;
        }
        if(l==r) return;
    }
    if(mid>=L) Modify(ls,l,mid,L,R,k,b);
    if(mid< R) Modify(rs,mid+1,r,L,R,k,b);
    T[u].Min=min(T[u].Min,min(T[ls].Min,T[rs].Min));
    return;
}

黑科技算法

1.斯坦那树

其实只是一种解决一类状压dp的方法。
当我们需要进行有关联通性的状压 dp 时 , 只要求关键点连通且关键点较少 , 但是非关键点比较多的时候 , 可以用到斯坦那树。
其实是一类不要求所有点连通而只要求一部分点连通的 \(MST\) , 只能状压来求。
状压关键点的连通性 , 并确定一个点已经在树中 , 那么在不影响关键点的状态 , 也就是相同状态中可以用最短路来转移 , 而不同状态一般固定一个点 , 枚举子集然后合并。
通过最短路巧妙解决了非关键之间连边的决策。

例题 [WC2008]游览计划

2.朱刘算法

最小树形图:

求解步骤:

首先要明确的一点是每一个点只会有一条入边。

  1. 为所有点找到一个最小的入边。
  2. 把这些边的权值加入答案 , 然后判断图中是否存在环。
  3. 不存在环那么算法结束 , 否则缩环(直接往入边方向跳即可,不要tarjan) , 并把所有不在环内边的边的权值减去指向点的最小入边(之前的环不合法 , 其中要去掉一条边 , 这里减去原入边边权就是在之后选择新的边的时候考虑了这个权值的变化)

代码:

#include<bits/stdc++.h>
using namespace std;
template<class T>inline void init(T&x){
    x=0;char ch=getchar();bool t=0;
    for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') t=1;
    for(;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+(ch-48);
    if(t) x=-x;return;
}
const int M=1e4+10;
const int N=101;
namespace Directed_MST{
    struct edge{int u,v,w;edge(){u=v=w=0;}edge(int a,int b,int c){u=a,v=b,w=c;}}a[M],b[M];
    int cnt=0;
    int ans=0;
    int In[N],bel[N],vis[N];
    int n,m,r,bcc=0;
    inline int MST(){
        for(int i=1;i<=n;++i) In[i]=bel[i]=vis[i]=0;bcc=0;
        for(int i=1;i<=m;++i) {// find minum_weight in_edge
            int u=a[i].u,v=a[i].v,w=a[i].w;
            if((!In[v])||a[In[v]].w>w) In[v]=i;
        }
        for(int u=1;u<=n;++u) {// judge circle and shrink them , sum the cost 
            if(!In[u]&&u!=r) return -1;
            if(u!=r) ans+=a[In[u]].w;
            if(vis[u]) continue;
            int v=u;
            for(;v^r&&vis[v]^u&&!bel[v];v=a[In[v]].u) vis[v]=u;// root needn't go back
            if(v^r&&!bel[v]) {// is a circle
                int s=v;bel[s]=++bcc;
                for(s=a[In[s]].u;v^s;s=a[In[s]].u) bel[s]=bcc;
            }
        }if(!bcc) return 0;
        for(int i=1;i<=n;++i) if(!bel[i]) bel[i]=++bcc;// other node
        cnt=0;
        for(int i=1;i<=m;++i){// shrink circle , create new edge
            int u=a[i].u,v=a[i].v,w=a[i].w;
            if(bel[u]==bel[v]) continue;
            int goi=a[In[v]].w;
            b[++cnt]=edge(bel[u],bel[v],w-goi);
        }
        for(int i=1;i<=cnt;++i) a[i]=b[i];m=cnt,cnt=0;
        n=bcc;return 1;
    }
    void work(){
        init(n),init(m);init(r);
        int u,v,w;
        for(int i=1;i<=m;++i) {init(u),init(v),init(w);if(u==v) --i,--m;else a[i]=edge(u,v,w);}
        int ret;
        while((ret=MST())==1) r=bel[r];
        if(~ret) printf("%d\n",ans);
        else puts("-1");
    }
}
int main(){Directed_MST::work();}

3.最小割树

\(Gemory-Hu\; Tree\)算法

对于一个 \(n\) 个节点的图 , 图中所有点对不同的最小割数目最多只有 \(n-1\) 个 , 可以证明存在一棵树 , 使得两点在这棵树上的最小割即为原图中的最小割 。
考虑3个点两两之间的最小割 \(C_{u,v},C_{u,t},C_{v,t}\) , 我们已知 \(C_{u,t},C_{v,t}\) , 假设在\(C_{u,v}\)中 ,不妨假设 \(t\) 被分在了与 \(v\) 在一起的割集 。由于在一个割中一个点一定被分在源点或者汇点的一侧割集 , 那么可以推出 \(C_{u,v}\leq C_{u,t}\) , 如果不是那么显然直接割掉 \(u,t\) 就能达到割掉 \(u,v\) 的目的而使最小割变小。
类似的可以得出 \(C_{u,v}\geq C_{u,t}\) , 那么只能是 \(C_{u,v}=C_{u,t}\)
用归纳法可以得到一个 \(n\) 个点的图中最多只有 \(n-1\) 个不同的最小割。

如何构建最小割树?

采用递归的策略 , 对于当前点集 , 任意取两个点做最小割(注意这里是对原图跑最小割) , 然后给这两个点连边 , 权值为最小割大小。
然后就把参与网络中与源点可达的点与源点扔在一起 , 与其他的和汇点扔在一起。两边递归即可。
正确性就是证明只有 \(n-1\) 个不同的最小割中的道理相同 , 考虑某一个点被划分在哪个集合从而保证了正确性。

模板题code:

#include<bits/stdc++.h>
using namespace std;
#define Set(a,b) memset(a,b,sizeof(a))
#define Copy(a,b) memcpy(a,b,sizeof(a))
template<class T>inline void init(T&x){
    x=0;char ch=getchar();bool t=0;
    for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') t=1;
    for(;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+(ch-48);
    if(t) x=-x;return;
}
const int M=1520;
const int N=520,INF=2e9;
int n,m;
int dis[N][N];
struct edge{
    int to,next,cap,flow;
}a[M<<1];
int cnt=0,head[N],cur[N];
inline void add(int x,int y,int z){a[cnt]=(edge){y,head[x],z,z};head[x]=cnt++;}
int d[N];
int que[N],tail=0,bel[N];
inline void Return(){for(int i=0;i<cnt;++i) a[i].cap=a[i].flow;for(int i=0;i<=n;++i) bel[i]=0;}
queue<int>Q;
int S,T;bool had[N];
inline bool bfs(){
    Set(d,0);
    while(!Q.empty()) Q.pop();
    Q.push(S);d[S]=1;
    while(!Q.empty()){
        int u=Q.front();Q.pop();
        for(int v,i=head[u];~i;i=a[i].next){
            v=a[i].to;if(d[v]||!a[i].cap) continue;
            d[v]=d[u]+1;Q.push(v);
        }
    }
    return d[T];
}
int dfs(int u,int flow){
    if(u==T) return flow;
    int rest=flow;
    for(int v,&i=cur[u];~i;i=a[i].next){
        v=a[i].to;if(d[v]!=d[u]+1||!a[i].cap) continue;
        int f=dfs(v,min(a[i].cap,rest));
        if(!f) d[v]=0;
        a[i].cap-=f,a[i^1].cap+=f;
        rest-=f;if(!rest) break;
    }
    return (flow-rest);
}
inline int Dinic(){
    int flow=0;
    while(bfs()) Copy(cur,head),flow+=dfs(S,INF);
    return flow;
}
void Dfs(int u){bel[u]=1;for(int v,i=head[u];~i;i=a[i].next){v=a[i].to;if(!a[i].cap||bel[v]) continue;Dfs(v);}return;}
namespace Gomory_Hu_Tree{
    struct edge{
        int to,next,w;
    }a[N<<1];int head[N],cnt=0;
    inline void add(int x,int y,int z){a[++cnt]=(edge){y,head[x],z};head[x]=cnt;}
    int tmp[N];
    inline void Divide(int l,int r){
        if(l>=r) return;int u;
        S=que[l],T=que[r];Return();int Flow=Dinic();Dfs(S);
        int L=l-1,R=r+1;add(S,T,Flow),add(T,S,Flow);
        for(int i=l;i<=r;++i) {
        u=que[i];if(bel[u]==1) tmp[++L]=u;else tmp[--R]=u;}for(int i=l;i<=r;++i) que[i]=tmp[i];
        Divide(l,L);Divide(R,r);
        return;
    }
    void DFS(int u,int fr,int fa,int Mi){
        dis[fr][u]=dis[u][fr]=Mi;
        for(int v,i=head[u];i;i=a[i].next){
            v=a[i].to;if(v==fa) continue;
            DFS(v,fr,u,min(Mi,a[i].w));
        }
        return;
    }
    inline void Work(){
        for(int i=0;i<=n;++i) que[++tail]=i;
        Divide(1,tail);return;
    }
    
}using Gomory_Hu_Tree::DFS; 
int main()
{
    init(n),init(m);
    int u,v,w;Set(head,-1);
    for(int i=1;i<=m;++i) {
        init(u),init(v),init(w);
        add(u,v,w),add(v,u,w);
    }
    Gomory_Hu_Tree::Work();
    int Q;init(Q);
    while(Q--){
        init(u),init(v);
        if(!had[u]&&!had[v]) DFS(u,u,0,INF),had[u]=1;
        printf("%d\n",dis[u][v]);
    }
    return 0;
}

4.灭绝树&支配树

如果钦定一个起点 \(S\) , 对于点 \(u\) 来说 , 如果点 \(p\) ,存在于任意一条从 \(S\)\(u\) 的路径上 , 也就是从 \(S\)\(u\) 必定经过点 \(p\) , 那么称 \(p\) 支配 \(u\) , 如果点 \(p\) 支配了 \(u\) 且不存在一个点 \(q\) 支配 \(u\) 且被 \(p\) 支配 , 那么称 \(p\)\(u\) 的支配点。

这样子一个点的支配点只有一个 , 就形成了一个树形结构。

  1. 树的支配树
    树的本身就是自己的支配树。

  2. \(DAG\) 的支配树
    也叫灭绝树。
    求解方法:
    增量法构造。一开始只有根节点。
    对原图进行拓扑排序 , 按照拓扑序 , 取出一个点时 , 所有和该点有边直接相连的点在已经构建好的支配树上的 \(LCA\) 就是这个点在支配树上的父亲。
    要求动态加叶子节点并维护 \(LCA\) , 动态处理倍增数组即可 , 当然你也可以写一个 \(LCT\)
    证明画个图很好理解。
    因为考虑了所有可能到达该点的点 , 求个 \(LCA\) 显然就是所有到达该点的必经点了。

  3. 一般有向图的支配树

这里是一种转成 \(DAG\) 然后照着 \(DAG\) 的方法做的方法。
我们定义半支配点:
\(semi[v]=min_{dfn}\{u| \exists path(u,v_1,v_2,v_3,\dots,v) \forall i,dfn[v_i]>dfn[v] \}\)
显然点 \(v\)\(dfs\)树 上的父亲已经满足了条件 , 但由于 \(dfs\) 序不是最小的所以不一定是它的半支配点。不过这也说明了一个点的半支配点的 \(dfs\) 序一定小于该点的 \(dfs\) 序。

如图:
\(dfs\) 访问顺序 :
\(dfn[\ ]=\{1,7,2,3,4,5,6,8,9\}\)

那么 \(5\) 号点的半支配点就是 \(7\) 号点 , 而其他点的半支配点都是 \(dfs\) 树上的父亲。

那么怎么求解半支配点?

首先要知道一些性质:

  1. \(dfs\)树有一个好的性质 , 就是没有被走过的边一定是由 \(dfn\) 大的指向 \(dfn\) 小的。
  2. 一个点的半支配点一定是该点在 \(dfs\) 树上的祖先 。 因为 \(dfs\) 树没有横叉边 , 而半支配点的 \(dfs\) 序要小于该点 , 那么只能是其祖先。
  3. 建立一个新图 , 保留 \(dfs\) 树后 , 从一个点的半支配点向该点连有向边形成一个 \(DAG\) , 在这张图上支配关系不发生改变。

感性理解:
首先为什么一个点的半支配点不一定是他的支配点?
我们假设 \(semi[v]=u\) , 即 \(u\)\(v\) 的半支配点
因为 \(u\) 只考虑了到达自己以后再到 \(v\) 这一段路 , 而有可能从该点的 \(dfs\) 树上的祖先能够通过其他路径到达 \(v\) , 因此可能存在它的祖先也能到达该点 。
在之后 , 半支配点意味着从该点出发 , 出去一开始搜到的路径 , 当路径长度大于 2 时 , 还存在另外几条路径到达点 \(v\)
由于我们半支配点取的是 \(dfs\) 树上最浅的点(\(dfs\)序最小) , 这样相当于把这些需要考虑的路径压缩在了一起 , 支配关系就是不变的。

正因为如此 , 所以我们的 \(DAG\) 还要保留原来的 \(dfs\) 树。以求出真正的支配点。

求解方法 , 先求出 \(dfs\) 序。
然后按照 \(dfs\) 序从大往小做。
要求一个点的半支配点 , 无非就是 \(dfs\) 树上的父亲或者是 , 走的是 \(dfs\) 序大于该点的一个路径的那个第一个还没有被考虑过的节点(就是 \(u\) 啦)。
\(dfs\)树好弄 , 那么怎么找回去的路?
我们枚举所有有指向 \(v\) 的边的那些点。
如果其 \(dfs\) 序小于 \(v\) , 那么肯定是 \(dfs\) 树上的祖先 , 直接用于更新半支配点(注意\(dfs\)序要最小) 。
如果不是那么之前一定已经考虑过了 , 容易发现半支配点是可以通过编号比自己大的点传递的 , 那么我们希望找到这条返回路径的开端 。 我们维护一个带权并查集 , 每次求完一个点后 , 把它与在 \(dfs\) 树上的祖先合并 , 这样就能找到反祖的路线 , 并且没有被考虑过的点不会合并到祖先上去 , 那么显然它就是路径的开端了 , 维护集合中 半支配点 \(dfs\)序 最小的点即可。
为什么要维护这个?图。

\(dfn[]=\{1,4,5,2,3\}\)

那么反着求解:
3号点的半支配点是 2 号点 , 2 号点的半支配点是 1 号点。
到了 5 号点 , 能够达到该点的有 3 号和 4 号。
此时 4 号点未被搜索过 , 这条路径端点即为 4 号点。
然后是 3 号点 , 它的反祖路径端点显然应该是 1 号节点 , 而 1 号节点当前仅为 2 号点的半支配点。而 5 号点的半支配点也是 1 号点。
必须记录过程中所有点半支配点中最小 \(dfs\) 的节点才能正确得到半支配点。
其实就是考虑了之前所有可能传递的半支配点信息 。 由于每次只是从往返回的边走一步的点上获取信息 , 保证了合法 , 再通过带权并查集来保证高效性与最优性。

然后就没有了。
另外很多题其实没有必要真的把支配树建出来 , 记个父亲然后拓扑排序就能解决问题 , 这样写会方便很多。

贴一个洛谷模板题的代码 (xzy orz)

#include<bits/stdc++.h>
using namespace std;
#define Set(a,b) memset(a,b,sizeof(a))
template<class T>inline void init(T&x){
    x=0;char ch=getchar();bool t=0;
    for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') t=1;
    for(;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+(ch-48);
    if(t) x=-x;
}
const int N=2e5+10;
const int M=4e5+10;
typedef long long ll;
struct edge{int to,next;};
int fa[20][N],dep[N],deg[N],son[N],n,m,que[N],tail=0,dfn[N],I=0,id[N],anc[N];
vector<int> Go[N];
queue<int> Q;
inline int LCA(int u,int v){
    if(dep[u]<dep[v]) swap(u,v);
    for(int k=18;~k&&dep[u]>dep[v];--k) if(fa[k][u]&&dep[fa[k][u]]>=dep[v]) u=fa[k][u];
    if(u==v) return u;
    for(int k=18;~k;--k) if(fa[k][u]&&fa[k][v]&&fa[k][u]!=fa[k][v]) u=fa[k][u],v=fa[k][v];
    return fa[0][u];
}
int size[N];
struct Graph{
    edge a[M];int head[N];int cnt;
    inline void clear(){cnt=0,Set(head,0);}
    inline void add(int x,int y){a[++cnt]=(edge){y,head[x]};head[x]=cnt;}
    inline void Topsort(){
        Q.push(1);
        while(!Q.empty()) {
            int u=Q.front();Q.pop();
            int sz=Go[u].size();
            if(sz) {
                int v=Go[u][0];
                for(int i=1;i<sz;++i) v=LCA(v,Go[u][i]);
                ++son[v];dep[u]=dep[v]+1;
                fa[0][u]=v;
                for(int k=1;k<=18;++k) fa[k][u]=fa[k-1][fa[k-1][u]];
            }
            for(int v,i=head[u];i;i=a[i].next){
                v=a[i].to;--deg[v];
                if(!deg[v]) Q.push(v);
                Go[v].push_back(u);
            }
        }
        for(int i=1;i<=n;size[i++]=1) if(!son[i]) Q.push(i);
        while(!Q.empty()) {
            int u=Q.front();Q.pop();
            int f=fa[0][u];if(!f) continue;
            size[f]+=size[u];
            --son[f];if(!son[f]) Q.push(f);
        }
        for(int i=1;i<=n;++i) printf("%d ",size[i]);puts("");
    }
    void dfs(int u){
        ++I;id[u]=I,dfn[I]=u;
        for(int v,i=head[u];i;i=a[i].next){v=a[i].to;if(id[v]) continue;anc[v]=u;dfs(v);}
        return;
    }
}G,IG,DAG;
int fs[N],semi[N],Mn[N];
int find(int x){
    if(fs[x]==x) return x;
    int t=fs[x];fs[x]=find(fs[x]);
    if(id[semi[Mn[t]]]<id[semi[Mn[x]]]) Mn[x]=Mn[t];
    return fs[x];
}
inline void Solve(){
    G.dfs(1);
    for(int i=1;i<=n;++i) {fs[i]=Mn[i]=semi[i]=i;size[i]=0;if(anc[i]) DAG.add(anc[i],i),++deg[i];}
    for(int i=I;i>1;--i) {
        int u=dfn[i],res=I;
        if(!u) continue;
        for(int v,j=IG.head[u];j;j=IG.a[j].next){
            v=IG.a[j].to;if(!id[v]) continue;
            if(id[v]<id[u]) res=min(res,id[v]);
            else {find(v),res=min(res,id[semi[Mn[v]]]);}
        }
        int v=dfn[res];
        semi[u]=v,fs[u]=anc[u];DAG.add(v,u);++deg[u];
    }
    DAG.Topsort();
    return;
}
int main(){
    init(n),init(m);
    int u,v;I=0;
    for(int i=1;i<=m;++i) {init(u),init(v);G.add(u,v);IG.add(v,u);}
    Solve();return 0;
}

猜你喜欢

转载自www.cnblogs.com/NeosKnight/p/10449510.html