欧拉路 一口气经过所有的点和边,且所有的边只经过一次
欧拉回路 能形成回路的欧拉路
如果我们令一个点的出度记为 1,入度记为 -1,这个点所有的出度和入度相加,就是它的度数
直接 dfs 整个图,注意所有的边只能走一次,然后在回溯时打印答案。有向图和无向图都一样
例题 UVA10054 无向图求欧拉回路,打印方案
cpp123456789101112131415161718192021222324252627282930313233343536373839404142#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define ls(k) (k)<<1
#define rs(k) (k)<<1|1
#define debug(x) cout<<#x<<"="<<x<<endl
using namespace std;
const int inf=0x3f3f3f3f3f3f3f3f;
const int mod=0;
typedef pair<int,int> pii;
int d[55],g[55][55];
void dfs(int u){
for(int v=1;v<=50;v++){
if(g[u][v]){
g[u][v]--,g[v][u]--;
dfs(v);
cout<<v<<" "<<u<<endl; // 回溯的时候
}
}
}
inline void subtask(){
int n;cin>>n;
while(n--){
int u,v;cin>>u>>v;
d[u]++,d[v]++;
g[u][v]++,g[v][u]++;
}
for(int i=1;i<=50;i++) if(d[i]%2) return cout<<"some beads may be lost"<<endl,void();
for(int i=1;i<=50;i++) if(d[i]) return dfs(i),void();
}
signed main(){
ios::sync_with_stdio(false);
int t;cin>>t;
for(int i=1;i<=t;i++){
cout<<"Case #"<<i<<endl;
subtask();
memset(d,0,sizeof(d));
memset(g,0,sizeof(g));
cout<<endl;
}
return 0;
}
如果递归深度过大的话,有可能会爆栈。此时我们考虑模拟递归时的栈来解决。基本思想是先从一个点一直往下dfs,dfs不动了,就把最后一个数字计入答案,然后再从第二个数字开始一直往下dfs
例题 POJ1780 有向图求欧拉路,打印方案,按字典序
题目大概意思是给定 然后让我们生成一个字符串,使得字符串从头开始每看 个数都是互不相同的 位数字,且覆盖所有的 位数字(含前导 0)
我们考虑现在已经有 位数字,然后填入下一位数字,此时就构成了一个 位数字,但同时后面的 位数字又变成了新的”状态“,等待填入下一位数字
因此我们可以把所有的 位数字视为一个状态,通过连有向边,即填数,来转移到另一个状态。每个状态向外连边只能经过一次,且需要把所有的边都走一遍,于是就转化为了欧拉路问题
cpp12345678910111213141516171819202122232425262728293031323334353637383940#include<iostream>
#include<algorithm>
#include<stack>
using namespace std;
#include <cassert>
int nxt[1000006],n,cnt;
stack<int> s,ans;
void dfs(int x){// 一直走下去
while(nxt[x]<10){
int to=(x*10+nxt[x])%cnt;
s.push(to);
nxt[x]++;
x=to;
}
}
int main(){
ios::sync_with_stdio(false);
while(cin>>n && n){
if(!n) break;
if(n==1){
cout<<"0123456789"<<endl;
continue;
}
cnt=1;
for(int i=1;i<=n-1;i++) cnt*=10;
for(int i=0;i<cnt;i++) nxt[i]=0;
for(int i=1;i<=n-1;i++) cout<<0;
dfs(0);
while(!s.empty()){// 走不动了,说明该回溯到上一个点,然后再向另一个方向一直走
ans.push(s.top()%10);s.pop();
if(!s.empty()) dfs(s.top());
}
int tot=n-1;
while(!ans.empty()) cout<<ans.top(),ans.pop(),tot++;
cout<<endl;
}
return 0;
}
先把点连出的所有边按照字典序排序,然后再求欧拉路,此时得到的方案其实就是按字典序的。但是由于我们是在回溯时记录答案,所以得到的方案其实是反的。所以我们还需要再把回溯时的要记录的点先放到一个栈里,然后最后再从栈中取出(相当于又反了一遍)
例题 P7771 有向图求欧拉路板子题,按字典序输出方案
cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748#include<bits/stdc++.h>
#define _ 200005
using namespace std;
inline int read(){
int res=0,ch=getchar(),f=1;
while(!isdigit(ch) and ch!=EOF){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
res=(res<<3)+(res<<1)+(ch-'0');
ch=getchar();
}
return res*f;
}
vector<int> ve[_];
int bl[_],n,m,in[_],out[_];
stack<int> st;
void dfs(int x){
for(int i=bl[x];i<ve[x].size();i=bl[x]){
bl[x]++;
dfs(ve[x][i]);
}
st.push(x);
}
int main(){
n=read(),m=read();
for(int i=1;i<=m;i++){
int u=read(),v=read();
in[v]++,out[u]++;
ve[u].push_back(v);
}
for(int i=1;i<=n;i++) sort(ve[i].begin(),ve[i].end());
int flag=0,cnt1=0,cnt2=0,s=0;
for(int i=1;i<=n;i++){
if(in[i]!=out[i]) flag=true;
if(in[i]-out[i]==1) cnt1++;
if(out[i]-in[i]==1) s=i,cnt2++;
}
if(flag&&!(cnt1==cnt2&&cnt1==1)) return cout<<"No",0;
if(flag) dfs(s);
else dfs(1);
while(!st.empty()) cout<<st.top()<<' ',st.pop();
return 0;
}
弧优化的dinic算法
cpp1234567891011121314151617181920212223242526272829int dis[_],rad[_],s,t;
bool bfs(){
memset(dis,0,sizeof(dis));dis[s]=1;
queue<int> q;q.push(s);
while(!q.empty()){
int now=q.front();q.pop();
rad[now]=head[now];
for(int i=head[now];i;i=edge[i].nxt){
int to=edge[i].to;
if(!dis[to]&&edge[i].w) dis[to]=dis[now]+1,q.push(to);
}
}
return dis[t]!=0;
}
int dfs(int now,int rem){
if(now==t) return rem;
int tmp=rem;
for(int i=rad[now];i;i=edge[i].nxt){
int to=edge[i].to;rad[now]=i;
if(dis[to]==dis[now]+1&&edge[i].w){
int k=min(edge[i].w,tmp);
int d=dfs(to,k);
edge[i].w-=d,edge[i^1].w+=d;
tmp-=d;
if(!tmp) break;
}
}
return rem-tmp;
}
如图所示,一种不相交的最小路径覆盖为 1->3->4、2、5
一个点也可以视为一个长度为 的路径
我们可以先建立一个二分图,左边是 ,右边也是 。如果 DAG 上存在一个有向边 的话,那么就把二分图中左边的 和右边的 连边。最后有
可以理解为,我先让每个点被自己覆盖。然后如果我在二分图上建立了一个匹配了,那么就相当于延长了一个路径,也就是少花费了一个点
还是如上图,一种可相交的最小路径覆盖为 1->3->4、2->3->5
我们先用 floyd 在图上搞传递闭包,然后如果 能到达 ,就在把二分图左边的 与右边的 连边。最后答案还是按上式计算
可以理解为如果 能到达 ,那么我们直接给连过去,先把中间的点省略,因为中间的点可能会被其他路径使用。如果后面的路径没使用到这个中间点的话,那么根据最大匹配的算法,一定会调整为选择使用中间的点,因为这样才能得到最大匹配
https://www.luogu.com.cn/problem/P2472
题目描述
在一个 行 列的网格地图中有一些高度不同的石柱,第 行 列的石柱高度为 。
一些石柱上站着一些蜥蜴,你的任务是让尽量多的蜥蜴逃到边界外。
每行每列中相邻石柱的距离为 ,蜥蜴的跳跃距离是 ,即蜥蜴可以跳到平面距离不超过 的任何一个石柱上。
石柱都不稳定,每次当蜥蜴跳跃时,所离开的石柱高度减 (如果仍然落在地图内部,则到达的石柱高度不变)。
如果该石柱原来高度为 ,则蜥蜴离开后消失,以后其他蜥蜴不能落脚。
任何时刻不能有两只蜥蜴在同一个石柱上。
数据范围
对于 的数据满足:,,。
笔记
题目总体来说有两个限制,一个是跳完之后高度就减一,减到 0 后就不能再跳到该位置了。另一个是同一时间只能有一个蜥蜴在一个柱子上
对于后者,其实可以忽略。如果我们能够得到一个蜥蜴的跳出方案,而这个蜥蜴要跳到的柱子已经被占用了,那么我们可以一直等待柱子解除占用,毕竟题目又没要求时间
于是就只需要考虑前面这个限制了。其实就是限制经过一个点的次数。对于这种要求,我们有如下建模方案:将点拆成入点和出点,然后入点到出点设置流量为限制次数的边
cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define ls(k) (k)<<1
#define rs(k) (k)<<1|1
#define debug(x) cout<<#x<<"="<<x<<endl
using namespace std;
const int inf=0x3f3f3f3f3f3f3f3f;
const int mod=0;
typedef pair<int,int> pii;
int n,m,d,g[22][22],s,t,has[22][22],siz=1,head[900],dis[900],rad[900];
inline int get(int x,int y,int t){
int res=(x-1)*m+y;
return t==0? res:n*m+res;
}
inline int dis2(int x1,int y1,int x2,int y2){return (x1-x2)*(x1-x2)+(y1-y2)*(y1-y2);}
struct Edge{int to,nxt,w;} edge[320004];
inline void _add(int u,int v,int w){edge[++siz].nxt=head[u],edge[siz].to=v,edge[siz].w=w,head[u]=siz;}
inline void add(int u,int v,int w){_add(u,v,w),_add(v,u,0);}
bool bfs(){
memset(dis,0,sizeof(dis));dis[s]=1;
queue<int> q;q.push(s);
while(!q.empty()){
int now=q.front();q.pop();
rad[now]=head[now];
for(int i=head[now];i;i=edge[i].nxt){
int to=edge[i].to;
if(!dis[to]&&edge[i].w) dis[to]=dis[now]+1,q.push(to);
}
}
return dis[t]!=0;
}
int dfs(int now,int rem){
if(now==t) return rem;
int tmp=rem;
for(int i=rad[now];i;i=edge[i].nxt){
int to=edge[i].to;rad[now]=i;
if(dis[to]==dis[now]+1&&edge[i].w){
int k=min(edge[i].w,tmp);
int d=dfs(to,k);
edge[i].w-=d,edge[i^1].w+=d;
tmp-=d;
if(!tmp) break;
}
}
return rem-tmp;
}
inline void subtask(){
cin>>n>>m>>d;
int cnt=0;
s=0,t=2*n*m+1;
for(int i=1;i<=n;i++){
string str;cin>>str;
for(int j=1;j<=m;j++) g[i][j]=str[j-1]-'0';
}
for(int i=1;i<=n;i++){
string str;cin>>str;
for(int j=1;j<=m;j++) has[i][j]=str[j-1]=='.'?0:1;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(!g[i][j]) continue;
if(has[i][j]) add(s,get(i,j,0),1),cnt++;
add(get(i,j,0),get(i,j,1),g[i][j]);
if(min(i,min(j,min(n-i+1,m-j+1)))<=d) add(get(i,j,1),t,inf);
for(int k=max(1ll,i-d);k<=min(i+d,n);k++){
for(int l=max(1ll,j-d);l<=min(j+d,m);l++){
if(k==i&&l==j) continue;
if(!g[k][l]) continue;
if(dis2(i,j,k,l)<=d*d) add(get(i,j,1),get(k,l,0),inf);
}
}
}
}
int ans=0;
while(bfs()) ans+=dfs(s,inf);
cout<<cnt-ans;
}
signed main(){
ios::sync_with_stdio(false);
int t=1;//cin>>t;
while(t--) subtask();
return 0;
}
https://www.luogu.com.cn/problem/P2754
题目描述
有 地球,月球,还有额外的 个星球,然后有 个宇宙飞船,可以分别以自己的某个顺序循环在几个星球之间穿梭,每次穿梭消耗一个单位的时间。每个飞船都有自己的容量限制
现在问至少需要多少时间才能把 个人从 地球 运往 月球
数据范围
笔记
先用并查集判断是否有解
然后题目中这个”时间“有点不好搞。这里又引出一个建模方法:根据时间分层,具体如图:
然后我们枚举时间,建图跑最大流,直到得到的流量等于 即可得到答案
注意理论上这种问题应该是二分答案的,但我们考虑直接枚举,因为如果有解,那么最大耗时不会超过 ,所以枚举可以。而且当我们枚举完一个答案再往下枚举的时候,我们可以直接在现有的图上补充点和边,而不用再重新建立一个完整的图,这是二分做不到的。而且我们跑完最大流后,补充点和边,直接再基于上一次的残留网络继续跑最大流就可以了。所以直接枚举的效率并不会太低
cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990#include<bits/stdc++.h>
#define ull unsigned long long
#define ls(k) (k)<<1
#define rs(k) (k)<<1|1
#define debug(x) cout<<#x<<"="<<x<<endl
using namespace std;
const int inf=0x3f3f3f3f3f3f3f3f;
const int mod=0;
typedef pair<int,int> pii;
#define _ 1000006
struct Edge{int nxt,to,w;} edge[_<<1];
int head[_],siz=1,opa[_],fa[_],las[_];
inline int find(int x){while(x!=fa[x]) x=fa[x]=fa[fa[x]];return x;}
inline void _add(int u,int v,int w){edge[++siz].nxt=head[u],edge[siz].to=v,edge[siz].w=w,head[u]=siz;}
inline void add(int u,int v,int w){_add(u,v,w),_add(v,u,0);}
int dis[200005],rad[_],s,t;
bool bfs(){
memset(dis,0,sizeof(dis));dis[s]=1;
queue<int> q;q.push(s);
while(!q.empty()){
int now=q.front();q.pop();
rad[now]=head[now];
for(int i=head[now];i;i=edge[i].nxt){
int to=edge[i].to;
if(!dis[to]&&edge[i].w) dis[to]=dis[now]+1,q.push(to);
}
}
return dis[t]!=0;
}
int dfs(int now,int rem){
if(now==t) return rem;
int tmp=rem;
for(int i=rad[now];i;i=edge[i].nxt){
int to=edge[i].to;rad[now]=i;
if(dis[to]==dis[now]+1&&edge[i].w){
int k=min(edge[i].w,tmp);
int d=dfs(to,k);
edge[i].w-=d,edge[i^1].w+=d;
tmp-=d;
if(!tmp) break;
}
}
return rem-tmp;
}
vector<int> to[_];
inline void subtask(){
int n,m,k;cin>>n>>m>>k;
for(int i=0;i<=n+1;i++) fa[i]=i;
for(int i=1;i<=m;i++){
cin>>opa[i];
int r;cin>>r;
for(int j=1,x;j<=r;j++){
cin>>x;
if(x==-1) x=1;
else if(x!=0) x+=1;
to[i].push_back(x);
}
for(int j=0;j<to[i].size();j++){
int u=to[i][j],v=to[i][(j+1)%to[i].size()];
u=find(u),v=find(v);
if(u!=v) fa[u]=v;
}
}
if(find(1)!=find(0)) return cout<<0,void();
s=0;
for(int i=0;i<=n+1;i++) las[i]=i+1;
int ans=0,base=n+3;
add(s,las[0],k);
int ok=0;
while(true){
ans++;
for(int i=0;i<=n+1;i++) add(las[i],base+i,inf);
for(int i=1;i<=m;i++){
int u=to[i][(ans-1)%to[i].size()];
int v=to[i][ans%to[i].size()];
add(las[u],base+v,opa[i]);
}
for(int i=0;i<=n+1;i++) las[i]=base+i;
t=base+1;
base=base+n+1+1;
while(bfs()) ok+=dfs(s,inf);
if(ok==k) return cout<<ans<<endl,void();
}
}
signed main(){
ios::sync_with_stdio(false);
int t=1;//cin>>t;
while(t--) subtask();
return 0;
}
https://www.luogu.com.cn/problem/P2765
题目描述
假设有 根柱子,现要按下述规则在这 根柱子中依次放入编号为 ,,,...的球“
每次只能在某根柱子的最上面放球。
同一根柱子中,任何 个相邻球的编号之和为完全平方数。
试设计一个算法,计算出在 根柱子上最多能放多少个球。例如,在 根柱子上最多可放 个球。
对于给定的 ,计算在 根柱子上最多能放多少个球。
数据范围
对于 的数据,保证 。
笔记
如果两个数能构成完全平方数,那么我们就把这两个数字连边,并规定一定是大的数字连向小的数字(其实小的数字往大的数字连边也可以,总之就是要保证一个顺序,但是大连小的话在后面补充建边时更有优势)
如果我们要判断答案是否为 ,那么就相当于现在我们有一个含有 个点的 DAG,并去判断这个 DAG 的最小不相交路径覆盖是否为
我们沿用上道题的做法,枚举答案。考虑答案增加一,那么我们就相当于二分图两边多了个点,然后把这个点连向相加可以为完全平方数的数,然后在仅对新增的这个点跑最大匹配就可以了
设当前点数为 ,当前最大匹配为 ,那么如果 说明 即为答案。 增加 , 可能增加 也可能不增加,所以 是单调不减的,所以直接枚举的正确性是可以保证的
cpp12345678910111213141516171819202122232425262728293031323334353637383940414243444546#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define ls(k) (k)<<1
#define rs(k) (k)<<1|1
#define debug(x) cout<<#x<<"="<<x<<endl
using namespace std;
const int inf=0x3f3f3f3f3f3f3f3f;
const int mod=0;
typedef pair<int,int> pii;
vector<int> ve[1000006];
int match[1000006],t[1000006],nxt[1000006];
bool dfs(int x,int tag){
if(t[x]==tag) return false;
t[x]=tag;
for(int to:ve[x]) if(!match[to]||dfs(match[to],tag)) return match[to]=x,true;
return false;
}
inline void subtask(){
int n;cin>>n;
int ans=0,ok=0,tag=0;
while(true){
ans++;
for(int i=2;i*i<2*ans;i++){
if(i*i<=ans) continue;
ve[ans].push_back(i*i-ans);
}
if(dfs(ans,++tag)) ok++;
if(ans-ok>n) break;
}
ans--;
cout<<ans<<endl;
for(int i=1;i<=ans;i++) nxt[match[i]]=i;
for(int i=1;i<=ans;i++){
if(match[i]) continue;
int now=i;cout<<now<<' ';
while(nxt[now]) cout<<nxt[now]<<' ',now=nxt[now];
cout<<endl;
}
}
signed main(){
ios::sync_with_stdio(false);
int t=1;//cin>>t;
while(t--) subtask();
return 0;
}
https://www.luogu.com.cn/problem/P2766
题目描述
给定正整数序列 。
令 为构造 时所使用的下标, 为构造 时所使用的下标。且 ,都有 ,。则 和 不同,当且仅当 ,使得 。
数据范围
笔记
直接使用 dp 跑最长不下降子序列。不用 做法的原因是 dp 的话我们就知道以每个数结尾能够形成的最长不下降子序列的长度是多少,这有助于我们计算方案
如果所有的数都可以重复取,那么我们可以用组合数学直接计算出来方案个数。但现在一个数只能取一次,即使考虑 dp 计算的话,后效性也太大
于是此时网络流又派上用场了,我们按 dp 转移阶段分层,然后如果一个点能够转移到另一个点,就连边。直接这么连的话有些点会被取多次。于是又是套路的拆点,限制经过点的流量
对于第三问,把边权改为 inf 后继续增广就可以了
cpp12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define ls(k) (k)<<1
#define rs(k) (k)<<1|1
#define debug(x) cout<<#x<<"="<<x<<endl
using namespace std;
const int inf=0x3f3f3f3f3f3f3f3f;
const int mod=0;
typedef pair<int,int> pii;
#define _ 505
int a[_],dp[_],siz=1,head[_],s,t;
struct Edge{int nxt,to,w;} edge[1000006];
inline void _add(int u,int v,int w){edge[++siz].nxt=head[u],edge[siz].to=v,edge[siz].w=w,head[u]=siz;}
inline void add(int u,int v,int w){_add(u,v,w),_add(v,u,0);}
int dis[_],rad[_];
bool bfs(){
memset(dis,0,sizeof(dis));dis[s]=1;
queue<int> q;q.push(s);
while(!q.empty()){
int now=q.front();q.pop();
rad[now]=head[now];
for(int i=head[now];i;i=edge[i].nxt){
int to=edge[i].to;
if(!dis[to]&&edge[i].w) dis[to]=dis[now]+1,q.push(to);
}
}
return dis[t]!=0;
}
int dfs(int now,int rem){
if(now==t) return rem;
int tmp=rem;
for(int i=rad[now];i;i=edge[i].nxt){
int to=edge[i].to;rad[now]=i;
if(dis[to]==dis[now]+1&&edge[i].w){
int k=min(edge[i].w,tmp);
int d=dfs(to,k);
edge[i].w-=d,edge[i^1].w+=d;
tmp-=d;
if(!tmp) break;
}
}
return rem-tmp;
}
inline void subtask(){
int n;cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) dp[i]=1;
int maxx=1;
for(int i=2;i<=n;i++) for(int j=1;j<i;j++) if(a[j]<=a[i]) dp[i]=max(dp[i],dp[j]+1),maxx=max(maxx,dp[i]);
cout<<maxx<<endl;
if(maxx==1) return cout<<n<<endl<<n,void();
s=0,t=2*n+1;
int e1,en;
for(int i=1;i<=n;i++){
add(2*i-1,2*i,1);
if(i==1) e1=siz-1;
if(i==n) en=siz-1;
if(dp[i]==1) add(s,2*i-1,inf);
if(dp[i]==maxx) add(2*i,t,inf);
for(int j=1;j<i;j++){
if(!(a[j]<=a[i]&&dp[j]==dp[i]-1)) continue;
add(2*j,2*i-1,1);
}
}
int ans=0;
while(bfs()) ans+=dfs(s,inf);
cout<<ans<<endl;
edge[e1].w=inf;
edge[en].w=inf;
while(bfs()) ans+=dfs(s,inf);
cout<<ans;
}
signed main(){
ios::sync_with_stdio(false);
int t=1;//cin>>t;
while(t--) subtask();
return 0;
}