图论相关

1.最短路

引入

Dijkstra

Dijkstra 是基于贪心的一种算法,适用于非负权的图,求解单源最短路。
算法是这样的:
找出全局距离最小的点 \(u\),并用这个点去更新连接它的 \(v\) 点的距离。
时间复杂度 \(O(n^2)\),使用堆优化可以优化到 \(O(m \log m)\)

code
priority_queue<pi> Q;
void dij(LL u) {
	memset(dis,63,sizeof dis);
	memset(vis,0,sizeof vis);
	dis[u]=0;
	Q.push(mk(0,u));
	for(; Q.size(); ) {
		LL x=Q.top().second; Q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(LL i=0; i<(LL)e[x].size(); i++) {
			LL y=e[x][i].first,z=e[x][i].second;
			if(dis[y]>dis[x]+z) {
				dis[y]=dis[x]+z; Q.push(mk(-dis[y],y));
			}
		}
	}
}
Bellman_Ford

是一种可以处理负权的单源最短路算法。
算法是这样的:
枚举每条边,并更新这条边上点,称为一次松弛。
重复上诉操作直到没有点被更新。
如果经过了 \(n\) 轮松弛后还存在有点被更新,则原图存在负环。
时间复杂度 \(O(nm)\).

SPFA

SPFA 是 Bellman_Ford 的队列优化。
松弛节点 \(x\) 时找到接下来有可能松弛的点,
即与 \(x\) 相邻且最短路被更新的点压入队列。
判断负环只需要是否存在点判断进入队列是否超过 \(n-1\) 次。
若有,则有负环。

在一般图上效率很高,但是会被构造数据卡成 \(O(nm)\).

code
void spfa() {
	memset(dis,63,sizeof dis);
	Q.push(s),dis[s]=0;
	for(; Q.size(); ) {
		int x=Q.front(); Q.pop();
		vis[x]=0;
		for(int i=head[x]; i; i=nxt[i]) {
			int y=ver[i],z=edge[i];
			if(dis[y]>dis[x]+z) {
				dis[y]=dis[x]+z;
				if(++cnt[y]>=n) return puts("NO"),0;
				if(!vis[y]) Q.push(y),vis[y]=1;
			}
		}
	}
}

应用

P5304 [GXOI/GZOI2019] 旅行者

如果我们设最短的最短路的两个点为 \(u,v\).
\(u\),\(v\) 在两个不同的部分。
那么我们只要建立一个起点 \(s\),向 \(u\) 部分建长度为 \(0\) 的边。
再建立一个终点 \(t\),让 \(v\) 的部分向 \(t\) 建长度为 \(0\) 的边。
\(s\rightarrow t\) 最短路即为 \(u\) 部分任一点到 \(v\) 部分任一点的最小值。
那么我们考虑将点分组。
可以随机分,设分 \(k\) 次,正确率是 \(1-(\frac{3}{4})^k\).
时间复杂度是 \(O(k\cdot m\log m)\).

code
#include<bits/stdc++.h>
#define pi pair<LL,LL>
#define mk make_pair
using namespace std;
using LL=long long;
const LL N=1e5+10,M=5e5+10;
const LL Rd=23;
LL T,n,m,k,e1,e2,t[N],p[N],dis[N];
bool vis[N];
vector<pi> e[N];
void init() {
	for(LL i=1; i<=n; i++) e[i].clear();
}
void addedge(LL u,LL v,LL w) {
	e[u].push_back(mk(v,w));
}
priority_queue<pi> Q;
void dij(LL u) {
	memset(dis,63,sizeof dis);
	memset(vis,0,sizeof vis);
	dis[u]=0;
	Q.push(mk(0,u));
	for(; Q.size(); ) {
		LL x=Q.top().second; Q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(LL i=0; i<(LL)e[x].size(); i++) {
			LL y=e[x][i].first,z=e[x][i].second;
			if(dis[y]>dis[x]+z) {
				dis[y]=dis[x]+z; Q.push(mk(-dis[y],y));
			}
		}
	}
}
void solve() {
	init();
	scanf("%lld%lld%lld",&n,&m,&k);
	for(LL i=1,u,v,w; i<=m; i++) {
		scanf("%lld%lld%lld",&u,&v,&w);
		addedge(u,v,w);
	}
	for(LL i=1; i<=k; i++) scanf("%lld",&t[i]);
	e1=n+1; e2=n+2;
	for(LL i=1; i<=k; i++) p[i]=t[i];
	LL ans=1e18;
	for(LL o=1; o<=Rd; o++) {
		random_shuffle(p+1,p+1+k);
		for(LL i=1; i<=k/2; i++)
			addedge(e1,p[i],0);
		for(LL i=k/2+1; i<=k; i++)
			addedge(p[i],e2,0);
		dij(e1);
		ans=min(ans,dis[e2]);
		e[e1].clear();
		for(LL i=k/2+1; i<=k; i++) {
			e[p[i]].erase(--e[p[i]].end());
		}
	}
	printf("%lld\n",ans);
}
signed main() {
	srand(1919);
	scanf("%lld",&T);
	for(; T; T--) solve();
	return 0;
}

正解是二进制分组,每次编号二进制第 \(i\)\(0\) 的分左边,为 \(1\) 的分右边。

P1266 速度限制

是分层图最短路。
即状态多加了一维。
可以看做第二维状态是不同的层。
每个层都是一个图。
层与层直接有联系。
整体上还是用 dijkstra 算法。

code
#include<bits/stdc++.h>
using namespace std;
const int N=155,M=24005,K=505;
int head[N],tot,nxt[M],ver[M],len[M],spd[M];
int pre[N][K][2];
double dis[N][K];
bool vis[N][K];
int n,m,d;
int tp,st[N];
struct node {
	int u,s;
	double t;
	bool operator < (const node A) const {return t>A.t;}
	node() {}
	node(int u_,int s_,double t_) {u=u_; s=s_; t=t_;}
} ;
priority_queue<node> Q;
void addedge(int u,int v,int w,int z) {
	ver[++tot]=v; nxt[tot]=head[u];
	spd[tot]=w; len[tot]=z;
	head[u]=tot;
}
void dij() {
	for(int i=0; i<=n; i++) for(int j=0; j<K; j++) dis[i][j]=1e18;
	pre[1][70][0]=-1;
	dis[1][70]=0;
	Q.push(node(1,70,dis[1][70]));
	for(; Q.size(); ) {
		int u=Q.top().u,s=Q.top().s;
		Q.pop();
		if(vis[u][s]) continue;
		vis[u][s]=1;
		for(int i=head[u]; i; i=nxt[i]) {
			int v=ver[i];
			if(spd[i]!=0) {
				if(dis[v][spd[i]]>dis[u][s]+1.0*len[i]/spd[i]) {
					dis[v][spd[i]]=dis[u][s]+1.0*len[i]/spd[i];
					pre[v][spd[i]][0]=u; pre[v][spd[i]][1]=s;
					Q.push(node(v,spd[i],dis[v][spd[i]]));
				}
			} else {
				if(dis[v][s]>dis[u][s]+1.0*len[i]/s) {
					dis[v][s]=dis[u][s]+1.0*len[i]/s;
					pre[v][s][0]=u; pre[v][s][1]=s;
					Q.push(node(v,s,dis[v][s]));
				}
			}
		}
	}
	int idx=0;
	for(int i=1; i<K; i++) {
		if(dis[d][i]<dis[d][idx]) idx=i;
	}
	for(int u=d,i=idx; ; ) {
		st[++tp]=u;
		int v=u,j=i;
		if(pre[v][j][0]==-1) break;
		u=pre[v][j][0]; i=pre[v][j][1];
	}
	for(int i=tp; i>=1; i--) printf("%d ",st[i]-1);
	puts("");
}
int main() {
	scanf("%d%d%d",&n,&m,&d); d++;
	for(int i=1,u,v,w,z; i<=m; i++) {
		scanf("%d%d%d%d",&u,&v,&w,&z);
		u++; v++; addedge(u,v,w,z);
	}
	dij();
	return 0;
} 

2.差分约束

引入

解决不定方程组,类似这样。

\( \begin{cases} x_2 \le x_1 -1 \\ x_3 \le x_1 +3 \\ x_3 \le x_2 +2 \end{cases} \)

我们发现这有点像三角形不等式,即求解完最短路后每条边满足的性质。
注:最短路的三角形不等式,即对于边 \(u\rightarrow v\),有 \(dis_v\le dis_u+w_{u,v}\),否则可以继续更新。
则我们建一个图:

\(d_1=0,d_2=-1,d_3=1\)
我们发现,这样求出来是对的。

不过,为了防止图不连通,我们建立源点,并向所有点连一条长 \(0\) 的边。
如果出现负环,那么就无解。
用 SPFA.

有一点技巧,若出现 \(x_1=x_2+1\) 这类等号,则等于 \(x_1 \le x_2+1\),且 \(x_1 \ge x_2+1\).

code
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+10;
int head[N],ver[N],nxt[N],edge[N],tot;
int n,m,cnt[N],dis[N];
bool vis[N];
queue<int> Q;
void addedge(int x,int y,int z) {
	ver[++tot]=y; edge[tot]=z;
	nxt[tot]=head[x]; head[x]=tot;
}
int main() {
	scanf("%d%d",&n,&m);
	for(int i=1,x1,x2,y; i<=m; i++) {
		scanf("%d%d%d",&x1,&x2,&y);
		addedge(x2,x1,y);
	}
	for(int i=1; i<=n; i++) Q.push(i),vis[i]=1,dis[i]=0;
	for(; Q.size(); ) {
		int x=Q.front(); Q.pop();
		vis[x]=0;
		for(int i=head[x]; i; i=nxt[i]) {
			int y=ver[i],z=edge[i];
			if(dis[y]>dis[x]+z) {
				dis[y]=dis[x]+z;
				if(++cnt[y]>=n) return puts("NO"),0;
				if(!vis[y]) Q.push(y),vis[y]=1;
			}
		}
	}
	for(int i=1; i<=n; i++) printf("%d ",dis[i]);
	puts("");
	return 0;
}

应用

UVA1723 Intervals

考虑前缀和,然后列出一些关系。
由于求最小解,那么用最长路求(笔者也不会证)。

code
#include<bits/stdc++.h>
using namespace std;
const int N=50010,M=2e5+10;
int t,k,n;
int head[N],tot,nxt[M],ver[M],edge[M];
int dis[N],vis[N];
void addedge(int x,int y,int z) {
	ver[++tot]=y;
	nxt[tot]=head[x];
	edge[tot]=z;
	head[x]=tot;
}
queue<int> Q;
void solve() {
	memset(head,0,sizeof head); tot=0;
	scanf("%d",&k);
	n=0;
	for(int i=1,a,b,c; i<=k; i++) {
		scanf("%d%d%d",&a,&b,&c);
		addedge(a,b+1,c);
		n=max(n,b);
	}
	for(int i=1; i<=n+1; i++) addedge(i-1,i,0);
	for(int i=1; i<=n+1; i++) addedge(i,i-1,-1);
	for(int i=0; i<=n+1; i++) dis[i]=-1e9;
	dis[0]=0; vis[0]=1; Q.push(0);
	for(; Q.size(); ) {
		int x=Q.front(); Q.pop();
		vis[x]=0;
		for(int i=head[x]; i; i=nxt[i]) {
			int y=ver[i],z=edge[i];
			if(dis[y]<dis[x]+z) {
				dis[y]=dis[x]+z;
				if(!vis[y]) vis[y]=1,Q.push(y);
			}
		}
	}
	printf("%d\n",dis[n+1]);
}
int main() {
	scanf("%d",&t);
	for(; t; t--) {
		solve();
		if(t!=1) puts("");
	}
	return 0;
}
P5590 赛车游戏

使得每条路径相同,条件是对于每条边 \(u\rightarrow v\)\(dis_v=dis_u+w_{u,v}\)
又因 \(1\le w_{u,v}\le 9\),那么转化为 \(dis_v \le dis_u+9,dis_v \ge dis_u+1\).
差分约束求解即可。

code
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10,M=2e3+10;
int n,m,vis[N],dis[N],cnt[N];
int head[N],from[M],nxt[M],ver[M],tot,val[N],viz[N];
vector<pair<int,int>> e[N];
queue<int> Q;
void addedge(int x,int y) {
	ver[++tot]=y; from[tot]=x;
	nxt[tot]=head[x]; head[x]=tot;
}
void aDD(int u,int v) {
	e[v].push_back(make_pair(u,-1));
	e[u].push_back(make_pair(v,9));
}
int dfs(int u) {
	viz[u]=1;
	if(val[u]||u==n) return val[u]=1;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if(val[v]) aDD(u,v),val[u]=1;
		else if(!viz[v]&&dfs(v)) aDD(u,v),val[u]=1;
	}
	return val[u];
}
int main() {
	scanf("%d%d",&n,&m);
	for(int i=1,u,v; i<=m; i++) {
		scanf("%d%d",&u,&v);
		addedge(u,v);
	}
	if(!dfs(1)) return puts("-1"),0;
	for(int i=1; i<=n; i++) vis[i]=1,Q.push(i),dis[i]=0;
	for(; Q.size(); ) {
		int x=Q.front(); Q.pop();
		vis[x]=0;
		for(int j=0; j<(int)e[x].size(); j++) {
			int y=e[x][j].first,z=e[x][j].second;
			if(dis[y]>dis[x]+z) {
				dis[y]=dis[x]+z;
				if(++cnt[y]>=n) return puts("-1"),0;
				if(!vis[y]) vis[y]=1,Q.push(y);
			}
		}
	} 
	printf("%d %d\n",n,m);
	for(int i=1; i<=m; i++) {
		int d=dis[ver[i]]-dis[from[i]];
		printf("%d %d %d\n",from[i],ver[i],(d<1||d>9)?1:d);
	}
	return 0;
} 
P4926 [1007] 倍杀测量者

注意到“有人女装”即为存在负环。
二分判断即可。

code
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-8;
const int N=1050;
struct flag{int o,a,b,k;} fl[N];
struct edge{int next,to; double w;} a[N<<1];
int n,s,t,c[N],fr[N],head[N],cnt,vis[N];
double dis[N];
queue<int> Q;
void link(int x,int y,double w) {a[++cnt]=(edge){head[x],y,w};head[x]=cnt;}
int check(double T) {
	memset(head,0,sizeof(head));
	memset(fr,0,sizeof(fr));cnt=0;
	while(!Q.empty()) Q.pop();
	for(int i=0; i<=n; i++) dis[i]=1,fr[i]=0,vis[i]=1,Q.push(i);	
	for(int i=1; i<=n; i++)
		if(c[i]) link(i,0,1.0/c[i]),link(0,i,c[i]);
	for(int i=1; i<=s; i++) {
		int A=fl[i].a,B=fl[i].b,k=fl[i].k,o=fl[i].o;
		if(o==1) link(B,A,k-T);
		else link(B,A,1.0/(k+T));
	}
	while(!Q.empty()) {
		int x=Q.front();
		for(int i=head[x]; i; i=a[i].next) {
			int R=a[i].to;
			if(dis[R]>=dis[x]*a[i].w) continue;
			dis[R]=dis[x]*a[i].w;fr[R]=fr[x]+1;
			if(fr[R]==n+2) return 1;
			if(!vis[R]) Q.push(R),vis[R]=1;
		}
		Q.pop(); vis[x]=0;
	}
	return 0;
}
int main(){
	cin>>n>>s>>t;
	double l=0,r=1e18,T=-1;
	for(int i=1; i<=s; i++) {
		int o,a,b,k;
		cin>>o>>a>>b>>k;
		fl[i]=(flag){o,a,b,k};
		if(o==1) r=min(r,(double)k-eps);
	}
	for(int i=1,C,x; i<=t; i++) cin>>C>>x,c[C]=x;
	while(r-l>eps) {
		double mid=(l+r)/2;
		check(mid)?l=T=mid:r=mid;
	}
	T==-1?puts("-1"):printf("%.10lf\n",T);
	return 0;
}

3.生成树

引入

kruscal

贪心,先将所有边按权从小到大排序,然后一一尝试加入。
若两点不连通,则加入。这里可以用并查集维护。

应用

P1967 [NOIP2013 提高组] 货车运输

跑最大生成树,然后树上倍增判断一下。

code
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,logn=15,M=5e4+10;
int n,m,q,fa[N],f[N][logn],g[N][logn],depth[N];
int s[N];
struct node {
	int u,v,w;
} e[M];
int tot,head[N],ver[2*N],nxt[2*N],edge[2*N];
int getf(int x) {
	if(x==fa[x]) return x;
	else return fa[x]=getf(fa[x]);
}
bool cmp(node p,node q) {
	return p.w>q.w;
}
void addedge(int x,int y,int z) {
	ver[++tot]=y;
	edge[tot]=z;
	nxt[tot]=head[x];
	head[x]=tot;
}
void dfs(int u,int z,int father) {
	depth[u]=depth[father]+1;
	f[u][0]=father; g[u][0]=z;
	for(int i=1; i<logn; i++) f[u][i]=f[f[u][i-1]][i-1];
	for(int i=1; i<logn; i++) g[u][i]=min(g[u][i-1],g[f[u][i-1]][i-1]);
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i],z=edge[i];
		if(v==father) continue;
		dfs(v,z,u);
	}
}
int Lca(int u,int v) {
    int ans=1e9;
	if(depth[u]<depth[v]) swap(u,v);
	for(int i=logn-1; i>=0; i--) {
		if(depth[f[u][i]]>=depth[v]) {
            ans=min(ans,g[u][i]);
            u=f[u][i];
        }
	}
	if(u==v) return ans;
    for(int i=logn-1; i>=0; i--) {
        if(f[u][i]!=f[v][i]) {
            ans=min(ans,min(g[u][i],g[v][i]));
            u=f[u][i]; v=f[v][i];
        }
    }
    if(u!=v) ans=min(ans,min(g[u][0],g[v][0]));
    return ans;
}
int main() {
	scanf("%d%d",&n,&m);
    memset(g,127,sizeof(g));
	for(int i=1; i<=m; i++) scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
	sort(e+1,e+1+m,cmp);
	for(int i=1; i<=n; i++) fa[i]=i;
	for(int i=1; i<=m; i++) {
		int fx=getf(e[i].u),fy=getf(e[i].v);
		if(fx!=fy) {
			addedge(e[i].u,e[i].v,e[i].w);
			addedge(e[i].v,e[i].u,e[i].w);
			fa[fx]=fy;
		}
	}
	for(int i=1; i<=n; i++) {
		fa[i]=getf(fa[i]);
		if(s[fa[i]]) continue;
		dfs(fa[i],1e9,0);
		s[fa[i]]=1;
	}
	scanf("%d",&q);
	for(int u,v; q; q--) {
		scanf("%d%d",&u,&v);
		if(fa[u]!=fa[v]) printf("-1\n");
		else {
			printf("%d\n",Lca(u,v));
		}
	}
    return 0;
}

4.连通性问题

引入

图的连通性算法

5.2-sat

引入

用于解决布尔方程组。
\( \begin{cases} p \wedge q = 1\\ p \wedge \neg q = 1 \end{cases} \)
如上面这个例子。
我们将一个点拆成两个点,对应取值为 \(0\)\(1\).
因为 \(p \wedge q = 1\) ,把 \(p_0\)\(q_1\) 建边,把 \(q_0\)\(p_1\) 建边。
因为 $p \wedge \neg q = 1 $,把 \(p_0\)\(q_0\) 建边,把 \(q_1\)\(p_0\) 建边。
建边 \(u\rightarrow v\) 的意思是如果有 \(u\) ,则 \(v\).

然后求解强连通分量。
若存在 \(p_0,p_1\) 在同一分量,那么无解。

如果有解,如何输出方案呢?
先拓扑排序,容易发现拓扑序较大的是最后取值,即存在 \(p(较小)\)\(p(较大)\) 的路径。
所以 \(p(较小)\) 能推出 \(p(较大)\)
所以对于 \(p_0,p_1\) 选择拓扑序较大那个。

对点编号可以用 \(i\),\(i+n\) 编号。

code
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,k;
int head[N],ver[N],nxt[N],tot;
int dfn[N],low[N],num,stk[N],ins[N],tp,c[N],scc;
void addedge(int x,int y) {
	ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot;
}
void tarjan(int u) {
	dfn[u]=low[u]=++num;
	stk[++tp]=u; ins[u]=1;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if(!dfn[v]) {
			tarjan(v);
			low[u]=min(low[u],low[v]);
		} else if(ins[v]) {
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(low[u]==dfn[u]) {
		scc++; int v;
		do {
			v=stk[tp--]; ins[v]=0;
			c[v]=scc;
		} while(u!=v);
	}
}
int main() {
	scanf("%d%d",&n,&k);
	for(int i=1,u,a,v,b; i<=k; i++) {
		scanf("%d%d%d%d",&u,&a,&v,&b);
		addedge(u+(a^1)*n,v+b*n);
		addedge(v+(b^1)*n,u+a*n);
	}
	for(int i=1; i<=2*n; i++) 
		if(!dfn[i]) tarjan(i);
	for(int i=1; i<=n; i++)
		if(c[i]==c[i+n]) return puts("IMPOSSIBLE"),0;
	puts("POSSIBLE");
	for(int i=1; i<=n; i++) {
		putchar(c[i+n]<c[i]?'1':'0');
		putchar(' ');
	}
	return 0;
}

应用

P3825 [NOI2017] 游戏

注意到如果没有 \(x\) 就是裸 2-sat.
那么枚举 \(x\) 的取值,注意这里不需要 abc 都枚举,只用枚举 ab,因为已经涵盖所有情况了。

code
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+10;
int n,d,k;
int p[30],cnt;
char s[N];
struct node {
	int u,a,v,b;
} r[N];
int head[N],nxt[N],ver[N],tot;
void addedge(int x,int y) {
	ver[++tot]=y;
	nxt[tot]=head[x];
	head[x]=tot;
}
int dfn[N],low[N],num,st[N],tp,ins[N],c[N],dcc;
void tarjan(int u) {
	dfn[u]=low[u]=++num;
	st[++tp]=u; ins[u]=1;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if(!dfn[v]) {
			tarjan(v); low[u]=min(low[u],low[v]);
		} else if(ins[v]) {
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(dfn[u]==low[u]) {
		++dcc; int v;
		do {
			v=st[tp--]; c[v]=dcc;
			ins[v]=0;
		} while(u!=v);
	}
}
bool solve() {
	memset(head,0,sizeof head); tot=0;
	for(int i=1; i<=k; i++) {
		int u=r[i].u,v=r[i].v;
		int a=-1,b=-1;
		if(s[u]=='a') {
			if(r[i].a==1) a=0; else if(r[i].a==2) a=1;
		} else if(s[u]=='b') {
			if(r[i].a==0) a=0; else if(r[i].a==2) a=1;
		} else if(s[u]=='c') {
			if(r[i].a==0) a=0; else if(r[i].a==1) a=1;
		}
		if(s[v]=='a') {
			if(r[i].b==1) b=0; else if(r[i].b==2) b=1;
		} else if(s[v]=='b') {
			if(r[i].b==0) b=0; else if(r[i].b==2) b=1;
		} else if(s[v]=='c') {
			if(r[i].b==0) b=0; else if(r[i].b==1) b=1;
		}
		if(a==-1) continue;
		if(b==-1) {
			addedge(a*n+u,(a^1)*n+u);
			continue;
		}
		addedge(a*n+u,b*n+v);
		addedge((b^1)*n+v,(a^1)*n+u);
	}
	memset(dfn,0,sizeof dfn); num=0;
	for(int i=1; i<=2*n; i++) 
		if(!dfn[i]) tarjan(i);
	for(int i=1; i<=n; i++)
		if(c[i]==c[i+n]) return false;
	for(int i=1; i<=n; i++) {
		if(s[i]=='a') putchar(c[i]<c[i+n]?'B':'C');
		else if(s[i]=='b') putchar(c[i]<c[i+n]?'A':'C');
		else if(s[i]=='c') putchar(c[i]<c[i+n]?'A':'B');
	}
	return true;
}
int main() {
	scanf("%d%d",&n,&d);
	scanf("%s",s+1);
	for(int i=1; i<=n; i++)
		if(s[i]=='x') p[cnt++]=i; 
	scanf("%d",&k);
	for(int i=1; i<=k; i++) {
		char a,b; int u,v;
		scanf("%d %c %d %c",&u,&a,&v,&b);
		r[i]={u,a-'A',v,b-'A'};
	}
	for(int mask=0; mask<(1<<d); mask++) {
		for(int i=0; i<d; i++) {
			if(mask&(1<<i)) s[p[i]]='a';
			else s[p[i]]='b';
		}
		if(solve()) return 0;
	}
	puts("-1");
	return 0;
}
P6378 [PA2010] Riddle

2-sat 考的就是建图。
每个部分用一下前缀优化建图。
先拆点,然后这样。

code
#include<bits/stdc++.h>
using namespace std;
const int N=8e6+10;
int n,m,k;
int head[N],ver[N],nxt[N],tot;
int a[N];
void addedge(int x,int y) {
	ver[++tot]=y;
	nxt[tot]=head[x];
	head[x]=tot;
}
int dfn[N],low[N],num,st[N],tp,ins[N],c[N],dcc;
void tarjan(int u) {
	dfn[u]=low[u]=++num;
	st[++tp]=u; ins[u]=1;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if(!dfn[v]) {
			tarjan(v); low[u]=min(low[u],low[v]);
		} else if(ins[v]) {
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(dfn[u]==low[u]) {
		++dcc; int v;
		do {
			v=st[tp--]; c[v]=dcc;
			ins[v]=0;
		} while(u!=v);
	}
}
int main() {
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1,u,v; i<=m; i++) {
		scanf("%d%d",&u,&v);
		addedge(u,v+n); addedge(v,u+n);
	}
	for(int i=1,w; i<=k; i++) {
		scanf("%d",&w);
		for(int i=1; i<=w; i++) scanf("%d",&a[i]);
		for(int i=1; i<=w; i++) {
			addedge(a[i]+n,a[i]+3*n);
			addedge(a[i]+2*n,a[i]);
		}
		for(int i=1; i<w; i++) {
			addedge(a[i]+3*n,a[i+1]);
			addedge(a[i+1]+n,a[i]+2*n);
			addedge(a[i]+3*n,a[i+1]+3*n);
			addedge(a[i+1]+2*n,a[i]+2*n);
		}
	}
	for(int i=1; i<=2*n; i++) if(!dfn[i]) tarjan(i);
	for(int i=1; i<=n; i++) if(c[i]==c[i+n]) return puts("NIE"),0;
	return puts("TAK"),0;
}
[ARC069F] Flags

二分 \(d\).
然后对于每个点,对其他距离小于 \(d\) 的点的另一个点建边。
这里可以用 ds 优化建图。
我用了分块。

code
#include<bits/stdc++.h>
using namespace std;
const int N=4e4+10,M=12e6+10;
int n,cnt;
int blk,bl[N],L[N],R[N],bo[N],opp[N];
struct node {
	int pos,id;
	bool operator < (const node s) const {
		return pos<s.pos;
	}
} p[N];
int head[N],ver[M],nxt[M],tot;
void addedge(int x,int y) {
	ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot;
}
int dfn[N],low[N],num,st[N],tp,ins[N],c[N],dcc;
void tarjan(int u) {
	dfn[u]=low[u]=++num;
	st[++tp]=u; ins[u]=1;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if(!dfn[v]) {
			tarjan(v); low[u]=min(low[u],low[v]);
		} else if(ins[v]) {
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(dfn[u]==low[u]) {
		++dcc; int v;
		do {
			v=st[tp--]; c[v]=dcc;
			ins[v]=0;
		} while(u!=v);
	}
}
bool cmp(node x,node y) {
	return x.pos<y.pos; 
}
void Link(int x,int l,int r) {
	if(l>r) return ;
	if(bl[l]==bl[r]) {
		for(int i=l; i<=r; i++) addedge(x,opp[p[i].id]);
		return ;
	}
	int Bl=bl[l],Br=bl[r];
	for(int i=Bl+1; i<=Br-1; i++) addedge(x,bo[i]);
	for(int i=l; i<=R[Bl]; i++) addedge(x,opp[p[i].id]);
	for(int i=L[Br]; i<=r; i++) addedge(x,opp[p[i].id]);
}
bool valid(int dis) {
	memset(head,0,sizeof head); tot=0;
	for(int i=1; i<=bl[2*n]; i++) {
		bo[i]=cnt+i;
		for(int j=L[i]; j<=R[i]; j++)
			addedge(bo[i],opp[p[j].id]);
	}
	for(int i=1; i<=2*n; i++) {
		int l_=upper_bound(p+1,p+1+cnt,node{p[i].pos-dis,0})-p;
		int r_=upper_bound(p+1,p+1+cnt,node{p[i].pos+dis-1,0})-p-1;
		Link(p[i].id,l_,i-1); Link(p[i].id,i+1,r_);
	}
	memset(dfn,0,sizeof dfn); num=dcc=0;
	for(int i=1; i<=2*n; i++) if(!dfn[i]) tarjan(i);
	for(int i=1; i<=n; i++) if(c[i]==c[opp[i]]) return false;
	return true;
} 
int main() {
	scanf("%d",&n); blk=sqrt(n+10);
	for(int i=1,a,b; i<=n; i++) {
		scanf("%d%d",&a,&b);
		p[++cnt]={a,i}; p[++cnt]={b,n+i};
		opp[i]=n+i; opp[n+i]=i;
	}
	sort(p+1,p+1+cnt,cmp);
	for(int i=1; i<=2*n; i++) {
		bl[i]=i/blk+1;
		if(!L[bl[i]]) L[bl[i]]=i;
		R[bl[i]]=max(R[bl[i]],i);
	}
	int l=0,r=1061109567,ans=0;
	while(l<=r) {
		int mid=(l+r)/2;
		if(valid(mid)) l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d\n",ans);
	return 0;
}

6.网络流

引入

网络流

7.点分治

引入

点分治

8.基环树

引入

基环树是一棵树加上一条边。
常见的维护手法是把环和非环分开讨论、把环缩点、断开环上边计算。

应用

P1453 城市环路

若是一颗树,可以树形 Dp。
先找环,然后环上每个点分别树形 Dp。
再断环成链,做一个环形 Dp。
设环有 \(m\) 个点。
钦定第一个点选,那么第 \(m\) 个点不选。
钦定第 \(m\) 个点选,那么第一个点不选。

code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,p[N],head[N],nxt[N<<1],ver[N<<1],tot;
double k;
void addedge(int x,int y) {
	ver[++tot]=y,nxt[tot]=head[x],head[x]=tot;
}
int vis[N];
int c[N],cnt;
int f[N][2],g[N][2],ans;
int findloop(int u,int fa) {
	if(vis[u]) return u;
	vis[u]=1;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if(v==fa) continue;
		int suf=findloop(v,u);
		if(suf) {
			c[++cnt]=u; vis[u]=2;
			return suf==u?0:suf;
		}
	}
	return 0;
}
void solve(int u,int fa) {
	f[u][1]=p[u],f[u][0]=0;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if(vis[v]==2||v==fa) continue;
		solve(v,u);
		f[u][1]+=f[v][0];
		f[u][0]+=max(f[v][0],f[v][1]);
	}
}
int main() {
	scanf("%d",&n);
	for(int i=1; i<=n; i++) scanf("%d",&p[i]);
	for(int i=1,u,v; i<=n; i++) {
		scanf("%d%d",&u,&v); u++,v++;
		addedge(u,v),addedge(v,u);
	}
	scanf("%lf",&k);
	findloop(1,1);
	for(int i=1; i<=cnt; i++) {
		solve(c[i],c[i]);
	}
	g[1][0]=f[c[1]][0],g[1][1]=-1e9;
	for(int i=2; i<=cnt; i++) {
		g[i][1]=g[i-1][0]+f[c[i]][1];
		g[i][0]=max(g[i-1][0],g[i-1][1])+f[c[i]][0];
	}
	ans=max(g[cnt][0],g[cnt][1]);
	g[1][0]=f[c[1]][0],g[1][1]=f[c[1]][1];
	for(int i=2; i<=cnt; i++) {
		g[i][1]=g[i-1][0]+f[c[i]][1];
		g[i][0]=max(g[i-1][0],g[i-1][1])+f[c[i]][0];
	}
	ans=max(ans,g[cnt][0]);
	printf("%.1lf\n",ans*k);
	return 0;
}
P4381 [IOI2008] Island

求基环树直径。

设环上有 \(m\) 个点。
\(d_i\) 为环上 \(i\) 子树距离 \(i\) 最远距离。

\(\max(d_i+d_j+dis(i,j))\)
\(dis(i,j)\) 可以用前缀和。

破坏成链,并把链加倍,即在 \([m+1,2m]\) 复制一遍。
计算出 \(s\) 为距离前缀和。
答案即为 \((d_i+s_i) + (d_j-s_j)\),其中 \(i-j<m\).
单调队列即可。

最后还要考虑环上子树中的直径。

code
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e6+10;
int n,y,z,tot=1,head[N],nxt[N],ver[N];
LL edge[N],w,dl[N],dis[N],f[N],s[N],ret;
int c[N],m,vis[N],is[N];
void addedge(int x,int y,LL z) {
	ver[++tot]=y;
	nxt[tot]=head[x];
	edge[tot]=z;
	head[x]=tot;
}
int findloop(int u,int in_edge) {
	if(vis[u]) return u;
	vis[u]=1;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if((in_edge^1)==i) continue;
		int suf=findloop(v,i);
		if(suf) {
			c[++m]=u; vis[u]=2;
			dl[m]=edge[i];
			return suf==u?0:suf;
		}
	}
	return 0;
}
int r,idx;
void getdis(int u,int f) {
	is[u]=1;
	for(int i=head[u]; i; i=nxt[i]) {
		int v=ver[i];
		if(v==f||vis[v]==2) continue;
		dis[v]=dis[u]+edge[i];
		if(dis[v]>dis[idx]||idx==0) idx=v;
		getdis(v,u); 
	}
}
LL calc() {
	LL ans=0;
	for(int i=1; i<=m; i++) {
		vis[c[i]]=1;
		idx=0,getdis(c[i],c[i]);
		f[i]=f[i+m]=dis[idx];
		int rt=idx;
		dis[rt]=0;
		idx=0,getdis(rt,rt);
		ans=max(ans,dis[idx]);
		vis[c[i]]=2;
	}
	for(int i=1; i<=2*m; i++) {
		if(i>m) dl[i]=dl[i-m];
		s[i]=s[i-1]+dl[i];
	}
	deque<int> q;
	for(int i=1; i<=2*m; i++) {
		while(q.size()&&i-q.front()>=m) q.pop_front();
		if(q.size()) ans=max(ans,f[i]+s[i]+f[q.front()]-s[q.front()]);
		while(q.size()&&f[q.back()]-s[q.back()]<=f[i]-s[i]) q.pop_back();
		q.push_back(i);
	}
	return ans;
}
int main() {
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1; i<=n; i++) {
		cin>>y>>z;
		addedge(i,y,z); addedge(y,i,z);
	}
	for(int i=1; i<=n; i++) {
		if(!is[i]) {
			m=0;
			findloop(i,0);
			ret+=calc();
		}
	}
	cout<<ret<<endl;
	return 0;
}
P8819 [CSP-S 2022] 星战

发现可以反攻是原图是内向基环树森林。
所有点出度为 \(1\)
我们发现出度是难以维护的,容易维护的是入度。
考虑每个点给一个随机权值 \(w_i\)
每个点要维护的是 \(f_i\),表示以 \(i\) 为终点的所有点 \(w\) 和。
所有点出度为 \(1\),那么边的起点包含了 \(1\sim n\).
\(\sum w =\sum f\)

code
#include<bits/stdc++.h>
using namespace std;
typedef unsigned int unt;
const int N=5e5+10;
int n,m,q;
unt val[N],w[N],s[N],sum,ans;
vector<int> e[N];
mt19937 rnd(time(0));
int main() {
	scanf("%d%d",&n,&m);
	for(int i=1; i<=n; i++) val[i]=rnd(),ans+=val[i];
	for(int i=1,u,v; i<=m; i++) {
		scanf("%d%d",&u,&v);
		w[v]+=val[u];
		sum+=val[u];
	}
	for(int i=1; i<=n; i++) s[i]=w[i];
	scanf("%d",&q);
	for(int t,u,v; q; q--) {
		scanf("%d",&t);
		if(t==1) {
			scanf("%d%d",&u,&v);
			sum-=val[u];
			w[v]-=val[u];
		} else if(t==2) {
			scanf("%d",&u);
			sum-=w[u];
			w[u]=0;
		} else if(t==3) {
			scanf("%d%d",&u,&v);
			sum+=val[u];
			w[v]+=val[u];
		} else {
			scanf("%d",&u);
			sum+=s[u]-w[u];
			w[u]=s[u];
		}
		if(sum==ans) puts("YES");
		else puts("NO");
	}
	return 0;
}
posted @ 2023-05-07 19:06  s1monG  阅读(11)  评论(0编辑  收藏  举报