神秘网络流卡常

· · 算法·理论

UPD:相比原文章修改了“HLPP 的堆”的错误复杂度。

前言

$n\le 10^6,m\le 3\times 10^6$ 是常见 $O(n^2m)$ 数据范围是吧。 $n\le 10^5$ 是常见 $O(n^2\log n)$ 数据范围是吧。 https://www.luogu.com.cn/training/1087835 这是题单。 ## 参考价值 以下内容: - $\color{green}\blacksquare$ 表示基础。 - $\color{#0afafb} \blacksquare$ 表示推荐。 - $\blue \blacksquare$ 表示在极端环境可以使用。 - $\red \blacksquare$ 表示在超级无敌巨大构式出题人毒瘤条件可以使用。 $$ \gdef \basict{\green{\blacksquare}} \gdef \good{\color{#0afafb} \blacksquare} \gdef \extr{\blue \blacksquare} \gdef \never{\red \blacksquare} $$ ## 网络流! ### $\color{#0afafb} \blacksquare$ ISAP ISAP 换 dinic。显然能快很多。 因为不仅少 bfs 常数,还有 gap 优化。 ### $\color{#0afafb} \blacksquare$ 非递归 ISAP 更快了!!实测快 $30\%$。 参考实现: ```cpp int isap(){ int flow=0,u=s; top=0; memset(buc,0,(t+1)<<2); bfs(); memcpy(cur,head,(t+1)<<2); while(lvl[s]<=t){ if(u==t){ int add=INT_MAX; for(int i=1;i<=top;i++)add=min(add,W[stk[i]]); for(int i=1;i<=top;i++){ W[stk[i]]-=add; W[stk[i]^1]+=add; } flow+=add; u=s; top=0; continue; } bool ok=0; for(int i=cur[u];i;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v; if(lvl[v]==lvl[u]-1&&W[i]&&lvl[v]>=0){ stk[++top]=i; u=v; ok=1; break; } } if(ok)continue; int minl=t; for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v; if(W[i]&&lvl[v]>=0)minl=min(minl,lvl[v]); } if(!--buc[lvl[u]])break; lvl[u]=minl+1; buc[lvl[u]]++; cur[u]=head[u]; if(u!=s){ int i=stk[top--]; u=ed[i^1].v; } } return flow; } ``` ### $\color{#0afafb} \blacksquare$ 小优化 刚才弹栈时可以记录最小值位置 $I$。然后不直接把 $u$ 设为 $0$ 而是设为 $rev(stk_I)$。$rev$ 表示反边。 可能少一些常数。和 dfs 一样。 ### $\color{#0afafb} \blacksquare$ 建模优化 有时出现: $s\to$ 白点,内部边,黑点 $ \to t$。 考察黑白点数量,如果白点过多,通过交换 $s,t$,尽量让 $s$ 连得少。 ### $\color{#0afafb} \blacksquare$ 双向边优化 常见网络流是 $u\xrightarrow{w} v$ 拆成 $u\xrightarrow xv$ 和 $u\xleftarrow 0v$。 但是 $u\xleftrightarrow wv$ 也可以直接两条边完事。 ### $\color{#0afafb} \blacksquare$ 建边优化 首先有分段加边。 可是有时候常数还是很大。因此其实可以按边权直接分类。 先建 $\infin$ 边,再建 $w$ 边,最后建 $0$ 边。 ### $\color{#0afafb} \blacksquare$ HLPP or ISAP?? 关于速度:稀疏图 ISAP,稠密图 HLPP。 ### $\good$ 卡空间? 对于匹配类型,正边边权为 $0$ 的和反边边权为 $1$ 的条数为 $O(n)$。 这样复杂度用 `std::set` 就多个 $\log n$。不过没关系,使用 [spn_set](https://www.luogu.me/paste/pnparijs) 就行了。大概有 $\dfrac1{150}$ 常数。 下面给出一个精简版本: :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; // by stripe_python #ifndef SPN_SET_H #define SPN_SET_H template <class T, class Cmp, const bool Unique> class _spn_set_base { private: static constexpr size_t _load = 150; Cmp cmp; size_t _size; vector<vector<T>> lists; vector<T> maxes; vector<size_t> index; void build () noexcept ; pair<size_t, size_t> pos (size_t) noexcept ; size_t loc (size_t, size_t) noexcept ; void split (size_t) noexcept ; void merge (size_t) noexcept ; void erase (size_t, size_t) ; public: _spn_set_base() : cmp(), _size(0) {} class iterator; iterator begin (); iterator end (); size_t size () const noexcept ; bool empty () const noexcept ; void clear () noexcept ; bool insert (const T&) ; bool erase (const T&) ; bool contains (const T&) noexcept ; }; template <class T, class Cmp, bool Unique> class _spn_set_base<T, Cmp, Unique>::iterator { public: _spn_set_base<T, Cmp, Unique> *set; size_t pos, idx; iterator() : set(nullptr), pos(-1), idx(-1) {} iterator(_spn_set_base<T, Cmp, Unique> *_set, size_t _pos, size_t _idx) : set(_set), pos(_pos), idx(_idx) {} operator size_t () {return set->loc(pos, idx);} T operator* () {return set->lists[pos][idx];} iterator& operator++ () { if (++idx >= set->lists[pos].size()) pos++, idx = 0; return *this; } iterator& operator-- () { if (--idx == (size_t) -1) pos--, idx = set->lists[pos].size() - 1; return *this; } bool operator==(const iterator& other) const { return set == other.set && pos == other.pos && idx == other.idx; } bool operator!=(const iterator& other) const { return !(*this == other); } iterator operator++(int) { iterator tmp = *this; ++(*this); return tmp; } iterator operator--(int) { iterator tmp = *this; --(*this); return tmp; } }; template <class T, class Cmp, bool Unique> typename _spn_set_base<T, Cmp, Unique>::iterator _spn_set_base<T, Cmp, Unique>::begin() { return typename _spn_set_base<T, Cmp, Unique>::iterator(this, 0, 0); } template <class T, class Cmp, bool Unique> typename _spn_set_base<T, Cmp, Unique>::iterator _spn_set_base<T, Cmp, Unique>::end() { return typename _spn_set_base<T, Cmp, Unique>::iterator(this, lists.size(), 0); } template <class T, class Cmp, bool Unique> void _spn_set_base<T, Cmp, Unique>::build() noexcept { size_t n = lists.size(); index.resize(n, 0); for (size_t i = 1; i < n; i++) { index[i] += lists[i - 1].size(); size_t x = i + (i & -i); if (x < n) index[x] += index[i]; } } template <class T, class Cmp, bool Unique> pair<size_t, size_t> _spn_set_base<T, Cmp, Unique>::pos(size_t idx) noexcept { if (idx < lists[0].size()) return {0, idx}; if (index.empty()) build(); size_t n = index.size(), p = 0; for (size_t i = __lg(n); i != (size_t) -1; i--) { size_t x = p + (1 << i); if (x < n && idx >= index[x]) idx -= index[x], p = x; } return {p, idx}; } template <class T, class Cmp, bool Unique> size_t _spn_set_base<T, Cmp, Unique>::loc(size_t pos, size_t idx) noexcept { if (pos == 0) return idx; if (index.empty()) build(); while (pos > 0) idx += index[pos], pos &= pos - 1; return idx; } template <class T, class Cmp, bool Unique> void _spn_set_base<T, Cmp, Unique>::split(size_t pos) noexcept { if (lists[pos].size() > (_load << 1)) { vector<T> half(lists[pos].begin() + _load, lists[pos].end()); lists[pos].erase(lists[pos].begin() + _load, lists[pos].end()); maxes[pos] = lists[pos].back(); lists.insert(lists.begin() + pos + 1, half); maxes.insert(maxes.begin() + pos + 1, half.back()); index.clear(); } else if (!index.empty()) { size_t n = index.size(); for (size_t i = pos + 1; i < n; i += (i & -i)) index[i]++; } } template <class T, class Cmp, bool Unique> void _spn_set_base<T, Cmp, Unique>::merge(size_t pos) noexcept { lists[pos - 1].insert(lists[pos - 1].end(), lists[pos].begin(), lists[pos].end()); maxes[pos - 1] = lists[pos - 1].back(); lists.erase(lists.begin() + pos), maxes.erase(maxes.begin() + pos), index.clear(); } template <class T, class Cmp, bool Unique> void _spn_set_base<T, Cmp, Unique>::erase(size_t pos, size_t idx) { lists[pos].erase(lists[pos].begin() + idx), _size--; size_t n = lists[pos].size(); if (n > (_load >> 1)) { maxes[pos] = lists[pos].back(); if (!index.empty()) { size_t n = index.size(); for (size_t i = pos + 1; i < n; i += (i & -i)) index[i]--; } } else if (lists.size() > 1) { if (pos == 0) pos++; merge(pos), split(pos - 1); } else if (n > 0) { maxes[pos] = lists[pos].back(); } else { lists.erase(lists.begin() + pos), maxes.erase(maxes.begin() + pos), index.clear(); } } template <class T, class Cmp, bool Unique> size_t _spn_set_base<T, Cmp, Unique>::size() const noexcept {return _size;} template <class T, class Cmp, bool Unique> bool _spn_set_base<T, Cmp, Unique>::empty() const noexcept {return maxes.empty();} template <class T, class Cmp, bool Unique> void _spn_set_base<T, Cmp, Unique>::clear() noexcept { _size = 0, lists.clear(), maxes.clear(), index.clear(); } template <class T, class Cmp, bool Unique> bool _spn_set_base<T, Cmp, Unique>::insert(const T& val) { if (maxes.empty()) { lists.emplace_back(1, val), maxes.emplace_back(val), _size++; return true; } size_t pos = upper_bound(maxes.begin(), maxes.end(), val, cmp) - maxes.begin(); #define _check(val0) do {if (!cmp(val, val0) && !cmp(val0, val)) return false;} while (0) if (pos == maxes.size()) { if (Unique) _check(maxes.back()); pos--, lists[pos].emplace_back(val), maxes[pos] = val; } else { auto it = upper_bound(lists[pos].begin(), lists[pos].end(), val, cmp); if (Unique) { if (it != lists[pos].begin()) _check(*prev(it)); else if (pos > 0) _check(lists[pos - 1].back()); } lists[pos].insert(it, val); } #undef _check _size++, split(pos); return true; } template <class T, class Cmp, bool Unique> bool _spn_set_base<T, Cmp, Unique>::erase(const T& val) { if (maxes.empty()) return false; size_t pos = lower_bound(maxes.begin(), maxes.end(), val, cmp) - maxes.begin(); if (pos == maxes.size()) return false; size_t idx = lower_bound(lists[pos].begin(), lists[pos].end(), val, cmp) - lists[pos].begin(); if (cmp(lists[pos][idx], val) || cmp(val, lists[pos][idx])) return false; erase(pos, idx); return true; } template <class T, class Cmp, bool Unique> bool _spn_set_base<T, Cmp, Unique>::contains(const T& val) noexcept { if (maxes.empty()) return false; size_t pos = lower_bound(maxes.begin(), maxes.end(), val, cmp) - maxes.begin(); if (pos == maxes.size()) return false; size_t idx = lower_bound(lists[pos].begin(), lists[pos].end(), val, cmp) - lists[pos].begin(); return !cmp(val, lists[pos][idx]) && !cmp(lists[pos][idx], val); } template <class T, class Cmp = less<T>> using spn_set = _spn_set_base<T, Cmp, true>; template <class T, class Cmp = less<T>> using spn_multiset = _spn_set_base<T, Cmp, false>; #endif //SPN_SET_H ``` ::: ### $\good$ HLPP 的堆? 狗都不用。 直接用桶维护吧!暴力找最大值,均摊下来 $O(n)$。 :::error[用了就爽变成 $\bm{O(n^2m/w)}$] ```cpp struct bueue{ int ma=-1,siz=0; vector<int> t[N]; bitset<N+5> vis; void push(int x,int y){ if(x>n)return; siz++; t[x].push_back(y); vis[n-x]=1; ma=max(ma,x); } void pop(){ siz--; t[ma].pop_back(); if(!t[ma].size())vis[n-ma]=0; ma=n-vis._Find_first(); } int size(){return siz;} int top(){return t[ma].back();} }; ``` ::: :::success[用了还是 $\bm{O(n^2\sqrt m)}$] ```cpp struct bueue{ int ma=-1,siz=0; vector<int> t[N]; bitset<N> vis; void push(int x,int y){ if(x>n)return; siz++; t[x].push_back(y); vis[n-x]=1; ma=max(ma,x); } void pop(){ siz--; t[ma].pop_back(); if(!t[ma].size()){ vis[n-ma]=0; ma=n-vis._Find_next(n-ma); } } int size(){return siz;} int top(){return t[ma].back();} void clear(int n){ siz=0,ma=-1; vis.reset(); for(int i=0;i<=n;i++)t[i].clear(); } }pq; ``` ::: ### $\good$ `std::vector` 存图与 `reserve(deg[u])` 我们知道 vector 很慢但是其实是因为没有 `reserve(SIZE)`,否则其实和数组一样快。([参考链接](https://www.luogu.me/article/b34zovu7)) vector 存图又有吊打链式前向星的内存连续性。 所以我们离线一下图然后 `reserve(deg[u])`。 ```cpp vector<array<int,3>> edges; edges.reserve(m); while(m--){ int u,v,w; u=read(),v=read(),w=read(); deg[u]++;deg[v]++; edges.push_back({u,v,w}); } for(int i=1;i<=n;i++)G[i].reserve(deg[i]); for(auto [u,v,w]:edges){ G[u].push_back({v,w,G[v].size()}); G[v].push_back({u,0,G[u].size()-1}); } ``` ### $\good $ 几种不同的 HLPP 实现? :::success[722ms] ```cpp int i=cur[u],sz=G[u].size(); while(i<sz){ auto &[v,w,fb]=G[u][i]; if(h[v]==h[u]-1&&w){//推 int use=min(w,ex[u]); w-=use; G[v][fb].w+=use; ex[u]-=use; ex[v]+=use; if(ex[v]==use&&v!=s&&v!=t){ cur[v]=0; pq.push(h[v],v); } } if(!ex[u]){ cur[u]=i; break; } if(w)mi=min(mi,h[v]); cur[u]=++i; } ``` ::: :::success[760ms] ```cpp for(int i=cur[u];i<G[u].size();i++){ auto &[v,w,fb]=G[u][i]; if(h[v]==h[u]-1&&w){//推 int use=min(w,ex[u]); w-=use; G[v][fb].w+=use; ex[u]-=use; ex[v]+=use; if(ex[v]==use&&v!=s&&v!=t){ cur[v]=0; pq.push(h[v],v); } } cur[u]=i; if(!ex[u])break; if(w)mi=min(mi,h[v]); } ``` ::: 容易发现快的那个对边的利用率更高。 ### $\basict$ 一些减少 HLPP 队列大小常数的? 你应该: ```cpp if(h[u]<=n)pq.push(h[u],u); ``` 而不是直接 push。 ### $\good$ 调节重标号次数的参数 可能会有些神秘作用?反正随机调参。 ### $\good$ 完全图叠加边权! 如下操作: ```cpp while(m--){ int u,v,w; cin>>u>>v>>w; val[u][v]+=w; deg[u]++;deg[v]++; } for(int i=1;i<=n;i++)G[i].reserve(deg[i]*2); for(int u=1;u<=n;u++){ for(int v=1;v<=n;v++){ if(val[u][v])adde(u,v,val[u][v]); } } ``` 另外,似乎**独立地存边权可以更优秀?** ### $\extr$ 网格图优化 不直接建边,而是直接存四个方向的权。 **这样在枚举四周时可以循环展开!** ### $\basict$ 还在用 `memset(A,B,sizeof(A))`?! 尝试 `memset(A,B,n<<3)` 吧。 如果要用 `long long` 的话,就吧 `sizeof(int)` 换成 `sizeof(long long)` 就行了。 ### $\basict$ 还在用 `for(int &i=cur[u];i<G[u].size();i++)`?! 不知道啥原理,反正不写 `&i` 而是手动 `cur[u]=i` 会快一些。 ## 费用流! ### $\color{#0afafb} \blacksquare$ 原始对偶 虽然是复杂度优化,$O(nmf)\to O(mf\log n)$。 但是确实我一般用来卡常就是了。 ### $\extr$ 更快的 dijkstra 用一个很快的堆!比如手写斐波那契堆或者配对堆。可以做到 $O(m+n\log n)$ 的 dijkstra。 总复杂度 $O((m+\log n)f)$。 ### $\extr$ 诗人啊?循环展开建图 参考:https://www.luogu.com.cn/record/286358799 我卡了一小会(~~指一个上午~~)。 推荐 wxx 大神的[题解](https://www.luogu.com.cn/article/hzr7vum4)。 ### $\extr$ 完全图上正常人就应该用链表优化的 dij! 常数小一点! ```cpp template <typename T> struct mylist{ struct data{T val;short nxt,pre;}; data a[N];//next pre int head=0; void init(short L,short R){ head=L; a[L]={0,L+1,-1}; for(int i=L+1;i<=R-1;i++)a[i]={inf,i+1,i-1}; a[R]={inf,-1,R-1}; } void del(int pos){ if(pos==head)head=a[pos].nxt; a[a[pos].pre].nxt=a[pos].nxt; a[a[pos].nxt].pre=a[pos].pre; } pair<T,short> qmin(T MAX){ T minn=a[head].val; short id=head; for(register int i=a[head].nxt;i!=-1&&minn;i=a[i].nxt){//L 是 s,不是 if(a[i].val<minn){ minn=a[i].val; id=i; } } return {minn,id}; } }; mylist<long long> q; queue<short> p; inline bool dijkstra(){ for(register int i=s;i<=t;++i)dis[i]=inf; dis[s]=0; q.init(s,t); for(register int i=s;i<=t;i++){ auto [d,u]=q.qmin(inf); if(u==-1)break; q.del(u); if(d==inf)break; for(int v:G0[u]){ bool flow=G[u][v].w; long long c=G[u][v].c; c+=h[u]-h[v]; if(dis[v]>dis[u]+c&&flow){ dis[v]=dis[u]+c; q.a[v].val=dis[v]; pre[v]=u; } } } vis.reset(); return dis[t]!=inf; } ``` ### $\never$ 我不明白 https://www.luogu.com.cn/discuss/1295818 为什么 EK 实现的 MCMF 能过啊。 *** :::info[对网络流进行 golf?]{open} P3376 【模板】网络最大流 AC 代码: (这还是一个兼顾长度和性能的 ISAP!用时:66ms) ```cpp #include<bits/stdc++.h> #define R return #define E(u,v,w)Y[C]={v,w,H[u]},H[u]=C++ using namespace std;using Z=long;const Z N=410,M=2e5;Z n,m,s,t,L[N],b[N],H[N],c[N],C=2,u,v,w,Q[N],l,r,i,U,F;array<Z,3>Y[M];void B(){b[L[Q[++r]=t]=1]=1;while(l<r){u=Q[++l];for(i=H[u];i;i=Y[i][2])v=Y[i][0],!L[v]&&Y[i^1][1]&&(L[v]=L[u]+1,b[L[v]]++,Q[++r]=v);}}Z D(Z u,Z A){if(u^t){Z F=0;for(Z i=c[u];i&&A;i=Y[i][2]){c[u]=i;auto&[v,w,_]=Y[i];L[v]==L[u]-1&&w&&(U=D(v,min(A,w)),U&&(w-=U,Y[i^1][1]+=U,A-=U,F+=U));}R A?b[L[u]]--,!b[L[u]]&&(L[s]=n+1),L[u]++,b[L[u]]++,F:F;}R A;}main(){for(cin>>n>>m>>s>>t;i<m;i++)cin>>u>>v>>w,E(u,v,w),E(v,u,0);B();while(L[s]<=n)memcpy(c,H,3e3),F+=D(s,1e18);cout<<F;} ``` :::