图论&树论做题记录

P4042 [AHOI2014/JSOI2014] 骑士游戏

乍一眼看着有点dp的感觉,但实际上会发现图建出来是会有环的,那直接dp肯定就寄了。

先把式子写出来:设 dpi 为消灭干掉第 i 个怪物的代价(包括后面生出来的),转移式 dpi=min(Ki,Si+Σj=1Ridpv,j)

观察一下,其实这个式子非常像,或者说就是最短路的松弛操作,那我们可以尝试通过跑最短路的方式来dp。

然后这道题似乎就这样了,但是还有些细节:一开始就把所有魔法攻击花费放进队列里、dp初值都赋成普通攻击花费、建边是 xi 而不是 ix(感性理解一下:所有的生出来的怪的花费都要回到最开始的怪计算)

然后最后的总花费直接取 dis1 就行了,因为是从 1 开始拓展的。最后记得开大数组就行了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*114514,M=1919810,inf=1145141919810;
ll n,s[N],k[N],r[N];
struct xx{
	ll next,to;
}e[2*M];
ll head[2*M],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll dis[N],vis[N];
typedef pair <ll,ll> pi;
priority_queue<pi,vector<pi>,greater<pi> > q;
ll dp[N]; //dp[i]为干掉第i个怪物的代价(包括后面生出来的) 
void dijkstra(){
	while(!q.empty()){
		ll u=q.top().second,val=q.top().first; q.pop();
		if(vis[u]) continue;
		vis[u]=1; dis[u]=val;
		for(int i=head[u];i;i=e[i].next){
			ll v=e[i].to;
			if(vis[v]||dp[v]>k[v]) continue; //还要注意判环
			dp[v]+=val; --r[v]; //解决一个
			if(r[v]==0) q.push(make_pair(dp[v],v)); 
		}
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		ll x;
		cin>>s[i]>>k[i]>>r[i];
		for(int j=1;j<=r[i];++j){
			cin>>x;
			add(x,i);
		}
		q.push(make_pair(k[i],i));
		dp[i]=s[i];
	}
	dijkstra();
	cout<<dis[1];
	return 0;
} 

P3163 [CQOI2014] 危桥

N50 已经明示了网络流,把危桥容量当成1建,然后源点 Sa1,b1an,bn 的边,汇点 T 同理。

但是这样子模拟一下会发现 a1 的流量会流到 b2a2 同理,所以不能只这么建。应该再将 b2,Sb1,T 连一起然后跑最大流,如果两次均有满流才有可行方案。

然后重跑一遍和多测记得清零。还有这个题是真的神奇,数组开到2505才过,之前还显示TLE/lh

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2505,M=1919810,inf=1145141919810;
ll n,a1,a2,an,b1,b2,bn; char c[N][N];
ll s,t;
struct xx{
	ll next,to,val;
}e[2*N];
ll head[N],cur[N],cnt=1;
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	head[x]=cnt;
}
ll dept[N],ans;
inline bool bfs(){
	queue <ll> q;
	memset(dept,0,sizeof(dept));
	q.push(s);
	dept[s]=1;
	cur[s]=head[s];
	while(!q.empty()){
		ll u=q.front(); q.pop();
		for(int i=head[u];i;i=e[i].next){
			ll v=e[i].to,w=e[i].val;
			if(!dept[v]&&w){
				q.push(v);
				dept[v]=dept[u]+1;
				cur[v]=head[v];
			}
		}
	}
	return dept[t];
}
inline ll dfs(ll u,ll val){
	if(u==t||!val) return val;
	ll use=0;
	for(int i=cur[u];i&&use<val;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(dept[v]==dept[u]+1&&w){
			ll res=dfs(v,min(val-use,w));
			if(!res) dept[v]=-1;
			e[i].val-=res;
			e[i^1].val+=res;
			val-=res;
			use+=res; 
		}
	}
	if(!use) dept[u]=0;
	return use;
}
void solve(){
	ans=0;
	memset(cur,0,sizeof(cur));
	memset(head,0,sizeof(head));
	cnt=1;
	++a1,++a2,++b1,++b2;
	s=n*n+1,t=s+1;
	add(s,a1,an),add(a1,s,an);
	add(s,b1,bn),add(b1,s,bn);
	add(a2,t,an),add(t,a2,an);
	add(b2,t,bn),add(t,b2,bn);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j){
			cin>>c[i][j];
			if(j<=i) continue;
			if(c[i][j]=='O') add(i,j,1),add(j,i,1);
			if(c[i][j]=='N') add(i,j,inf),add(j,i,inf);
		}
	while(bfs()) ans+=dfs(s,inf);
	if(ans!=an+bn){
		cout<<"No\n";
		return;
	}
	ans=0;
	memset(cur,0,sizeof(cur));
	memset(head,0,sizeof(head));
	cnt=1;
	swap(b1,b2);
	add(s,a1,an),add(a1,s,an);
	add(s,b1,bn),add(b1,s,bn);
	add(a2,t,an),add(t,a2,an);
	add(b2,t,bn),add(t,b2,bn);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j){
			if(j<=i) continue;
			if(c[i][j]=='O') add(i,j,1),add(j,i,1);
			if(c[i][j]=='N') add(i,j,inf),add(j,i,inf);
		}
	while(bfs()) ans+=dfs(s,inf);
	if(ans!=an+bn) cout<<"No\n";
	else cout<<"Yes\n";
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	while(cin>>n>>a1>>a2>>an>>b1>>b2>>bn) solve();
	return 0;
}

P2472蜥蜴

这个题也是乍一看不知道怎么建模。

对于每个点,珂以将其拆成一个连接所有入度的点和一个连接所有出度的点,两点之间的容量为h。

然后可以直接 O(n4) 枚举两个点的距离是否小于等于 d,然后连一条容量为inf的边。

把每个有蜥蜴的点都连到一个超级源点上,边容量为一。把所有能跳出边界的点都连到一个超级汇点上,边容量inf

然后算出的最大流就是能跑掉的蜥蜴数,用总蜥蜴数减去就可得出答案。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2001,M=1919810,inf=1145141919;
ll r,c,d,s,t,ans; char ch;
ll h[N][N]; bool f[N][N];
struct xx{
	ll next,to,val;
}e[1145140];
ll head[2*N],cnt=1;
ll id(ll i,ll j){
	return c*(i-1)+j;
} 
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	head[x]=cnt;
	//
	e[++cnt].next=head[y];
	e[cnt].to=x;
	e[cnt].val=0; //超忘了反向边容量0 
	head[y]=cnt;
}
ll dept[N],cur[N];
queue <ll> q;
bool bfs(){
	memset(dept,0,sizeof(dept));
	q.push(s);
	dept[s]=1;
	cur[s]=head[s];
	while(!q.empty()){
		ll u=q.front(); q.pop();
		for(int i=head[u];i;i=e[i].next){
			ll v=e[i].to,w=e[i].val;
			if(!dept[v]&&w){
				q.push(v);
				dept[v]=dept[u]+1;
				cur[v]=head[v];
			}
		}
	}
	return dept[t];
}
ll dfs(ll u,ll val){
	if(u==t) return val;
	ll use=0;
	for(int i=cur[u];i&&use<val;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(dept[v]==dept[u]+1&&w){
			ll res=dfs(v,min(val-use,w));
			if(!res) dept[v]=-1;
			e[i].val-=res;
			e[i^1].val+=res;
			val-=res;
			use+=res; 
		}
	}
	return use;
}
ll dist(double x,double y,double xx,double yy){
	return ceil(sqrt(pow(x-xx,2)*1.0+pow(y-yy,2)));
}
int main(){
	cin>>r>>c>>d;
	s=0,t=r*c*2+1;
	for(int i=1;i<=r;++i)
		for(int j=1;j<=c;++j){
			cin>>ch;
			ll h=ch-'0';
			if(h){
				add(id(i,j),id(i,j)+r*c,h);
				for(int k=1;k<=r;++k)
					for(int l=1;l<=c;++l){
						if(k==i&&l==j) continue;
						if(dist(i,j,k,l)<=d) add(id(i,j)+r*c,id(k,l),inf); //不同点之间容量设inf 
					}
				if(i+d>r||i-d<1||j+d>c||j-d<1) add(id(i,j)+r*c,t,inf); //能超出边界直接连汇点 
			}
		}
	for(int i=1;i<=r;++i)
		for(int j=1;j<=c;++j){
			cin>>ch;
			if(ch=='L'){
				++ans;
				add(s,id(i,j),1);
			}
		}
	while(bfs()){
		ll x;
		while((x=dfs(s,inf))) ans-=x;
	} //用总蜥蜴数减去能跑掉的蜥蜴数
	cout<<ans;
	return 0;
}

P2469 [SDOI2010] 星际竞速

还是一眼下去不知道怎么建图……但是肯定是要拆点的。把每个点拆成连接s和t的两个点。边的容量都设成1就好

首先建一个源点,向所有点连一条费用0的边,这样从s开始可以保证流过每一个点,还要向 i+n 建一条费用 Ai 的边。

然后每个点从 i+n 连到 t,费用为0。至于连边的时候连 uv+n,费用为 w,还要注意加边的顺序,保证编号小的点连向编号大的点。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1605,M=1919810;
ll n,m,s,t,inf;
ll dis[N],vis[N],a[N];
ll ans;//最大流
ll res;//最小费用 
struct xx{
	ll next,to,val,cost;
}e[2*M];
ll cnt=1,head[2*M],cur[2*M];
void add(ll x,ll y,ll z,ll w){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	e[cnt].cost=w;
	head[x]=cnt;
	//
	e[++cnt].next=head[y];
	e[cnt].to=x;
	e[cnt].val=0;
	e[cnt].cost=-w;
	head[y]=cnt;
}
bool spfa(){
	queue <ll> q;
	q.push(s);
	memset(dis,63,sizeof(dis));
	inf=dis[s];
	dis[s]=0,vis[s]=1;
	while(!q.empty()){
		ll u=q.front(); q.pop();
		vis[u]=0;
		for(int i=head[u];i;i=e[i].next){
			ll v=e[i].to,w=e[i].val,c=e[i].cost;
			if(dis[v]>dis[u]+c&&w){
				dis[v]=dis[u]+c;
				if(!vis[v]) q.push(v),vis[v]=1;
			}
		}
	}
	if(dis[t]!=inf) return 1;
	return 0;
}
bool vi[N];
ll dfs(ll u,ll val){
	if(u==t) return val;
	vi[u]=1;
	ll ans=0;
	for(int i=head[u];i&&ans<val;i=e[i].next){
		ll v=e[i].to,w=e[i].val,c=e[i].cost;
		if(!vi[v]&&dis[v]==dis[u]+c&&w){
			ll x=dfs(v,min(w,val-ans));
			if(!x) dis[v]=-1;
			res+=x*e[i].cost,e[i].val-=x;
			e[i^1].val+=x,ans+=x;
		}
	}
	vi[u]=0;
	return ans;
}
/*应该要拆点,把每个点拆成连s和t的,然后把边的容量都看成1*/
int main(){
	cin>>n>>m; s=0; t=1604;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		add(s,i,1,0),add(s,i+n,1,a[i]);
		add(i+n,t,1,0);
	}
	for(int i=1;i<=m;++i){
		ll u,v,w;
		cin>>u>>v>>w;
		if(u>v) swap(u,v);
		add(u,v+n,1,w);
	}
	while(spfa()){
		ll x;
		while((x=dfs(s,inf))) ans+=x;
	}
	cout<<res;
	return 0;
} 

P2604 [ZJOI2010] 网络扩容

这个要简单一些,先把费用当成 0 跑一遍最大流,然后新建一个汇点将原汇点向他连一条容量 k 的边,其他边多连一条容量 inf 费用 c[i] 的边。

注意就是不能清空原图,这个我不太清楚原理,但是珂以感性理解一下:如果直接建 inf 的边那么就不会像原来的图一样流了,就只是看费用大小而忽视了流的情况。

实际上好像说的和这个差不多,因为它是要在残量网络上跑什么的。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810;
ll n,m,k,s,t,inf;
ll dis[N],vis[N],a[N];
ll ans;//最大流
ll res;//最小费用 
struct xx{
	ll next,to,val,cost;
}e[2*M];
ll cnt=1,head[2*M],cur[2*M];
void add(ll x,ll y,ll z,ll w){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	e[cnt].cost=w;
	head[x]=cnt;
	//
	e[++cnt].next=head[y];
	e[cnt].to=x;
	e[cnt].val=0;
	e[cnt].cost=-w;
	head[y]=cnt;
}
bool spfa(){
	queue <ll> q;
	q.push(s);
	memset(dis,63,sizeof(dis));
	inf=dis[s];
	dis[s]=0,vis[s]=1;
	while(!q.empty()){
		ll u=q.front(); q.pop();
		vis[u]=0;
		for(int i=head[u];i;i=e[i].next){
			ll v=e[i].to,w=e[i].val,c=e[i].cost;
			if(dis[v]>dis[u]+c&&w){
				dis[v]=dis[u]+c;
				if(!vis[v]) q.push(v),vis[v]=1;
			}
		}
	}
	if(dis[t]!=inf) return 1;
	return 0;
}
bool vi[N];
ll dfs(ll u,ll val){
	if(u==t) return val;
	vi[u]=1;
	ll ans=0;
	for(int i=head[u];i&&ans<val;i=e[i].next){
		ll v=e[i].to,w=e[i].val,c=e[i].cost;
		if(!vi[v]&&dis[v]==dis[u]+c&&w){
			ll x=dfs(v,min(w,val-ans));
			if(!x) dis[v]=-1;
			res+=x*e[i].cost,e[i].val-=x;
			e[i^1].val+=x,ans+=x;
		}
	}
	vi[u]=0;
	return ans;
}
ll u[N],v[N],w[N],c[N];
int main(){
	cin>>n>>m>>k; s=1,t=n;
	for(int i=1;i<=m;++i){
		cin>>u[i]>>v[i]>>w[i]>>c[i];
		add(u[i],v[i],w[i],0);
	}
	while(spfa()){
		ll x;
		while((x=dfs(s,inf))) ans+=x;
	}
	cout<<ans<<" ";
	t=n+1,res=0;
	//memset(head,0,sizeof(head)); 不能清空欸 
	//memset(cur,0,sizeof(cur)); cnt=1;
	add(n,t,k,0); //保证流量最大为k
	for(int i=1;i<=m;++i) add(u[i],v[i],inf,c[i]);
	while(spfa()){
		ll x;
		while((x=dfs(s,inf))) ans+=x;
	}
	cout<<res;
	return 0;
} 

P1954 [NOI2010] 航空管制

淦直接用vector搞第一问错了/fn

第一问其实没什么说的,把 k 值改成满足每个约束的值,即 k[i]<=k[j]1。然后跑一遍dfs就行了,我一开始直接用vector就有可能出现顺序的问题,导致一个 k 先更新结果后面本该比它大的 k 甚至比他小了。

第二问就很智慧。先建出原图的反图,然后每次dfs遍历,遍历不到的点就都是能给当前点空出时间的。

然后就没了。还有一道[ABC304Ex] Constrained Topological Sort跟他有点相似。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*114514,M=1919810;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],hea[2*N],cnt,vis[N];
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
void adda(ll x,ll y){
	e[++cnt].next=hea[x];
	e[cnt].to=y;
	hea[x]=cnt;
}
ll n,m,du[N],ans[N],k[N];
vector <ll> g[N],gp[N];
void dfs(ll u){
	vis[u]=1;
	for(int i=hea[u];i;i=e[i].next){
		ll v=e[i].to;
		if(vis[v]) continue;
		dfs(v);
	}
}
ll dfs1(ll u){
	if(vis[u]) return k[u];
	vis[u]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to,w=0;
		w=dfs1(v);
		k[u]=min(k[u],w-1);
	}
	gp[k[u]].push_back(u);
	return k[u];
}
int main(){
	//freopen("Aha.in","r",stdin);
	//freopen("Aha.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>k[i];
	for(int i=1;i<=m;++i){
		ll a,b;
		cin>>a>>b; ++du[b];
		add(a,b),adda(b,a); g[a].push_back(b); //第一问错了…… 
	}
	/*for(int i=1;i<=n;++i)
		for(int j:g[i])
			k[i].val=min(k[i].val,k[j].val-1);
	sort(k+1,k+n+1,cmp);*/
	for(int i=1;i<=n;++i) dfs1(i);
	for(int i=1;i<=n;++i)
		for(int j:gp[i])
			cout<<j<<" ";
	cout<<'\n';
	//离散化没屁用 
	for(int i=1;i<=n;++i){
		ll cnt=n;
		for(int j=1;j<=n;++j) vis[j]=0;
		dfs(i); //建反向边后dfs完vis为0的点都是能给当前的i留出一个时间的 
		for(int j=n;j>=1;--j){ //枚举k值 
			if(cnt>j) break;
			for(int l=0;l<gp[j].size();++l)
				if(!vis[gp[j][l]]) --cnt;
		}
		cout<<cnt<<" ";
	}
	//for(int i=1;i<=n;++i) cout<<ans[i]<<" ";
	return 0;
}

P2886

智慧的Floyd。首先看到边数只有 100 就珂以考虑Floyd,但是点最大有 1000,那就整一个离散化。

然后,

矩 阵 快 速 幂 ?

是这样的,我们就不用最初的Floyd了,而是转成用类似他的方式来进行矩阵相乘。

可以这么想,我们有 f[i][j]=min(f[i][j],a[i][k]+b[k][j]),其中 a 表示经过 x 条边的最短路,b 表示经过 y 条边的最短路,那么 f 就是经过 x+y 的最短路了。

然后用快速幂的形式进行对长度为 len 的最短路径的计算,称作矩 阵 快 速 幂。

好吧其实借鉴了一下题解,不过这个转化确实有点妙,但是如果不知道这种操作我是完全不可能能单独想出来的。算了,知道了就好,记住了下次就能用了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145,M=1919810;
ll n,m,s,t,len;
ll u[N],v[N],w[N],pos[M];
ll edge[N][N],res[N][N],f[N][N];
void a(){ //floyd
	memset(f,63,sizeof(f));
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			for(int k=1;k<=n;++k)
				f[i][j]=min(f[i][j],edge[i][k]+edge[k][j]);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			edge[i][j]=f[i][j];
}
void ans(){
	memset(f,63,sizeof(f));
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			for(int k=1;k<=n;++k)
				f[i][j]=min(f[i][j],edge[i][k]+res[k][j]);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			res[i][j]=f[i][j];
}
ll qpow(ll len){
	while(len){
		if(len&1) ans();
		a();
		len>>=1;
	}
	return res[s][t];
}
int main(){
	memset(edge,63,sizeof(edge)),memset(res,63,sizeof(res));
	for(int i=1;i<=1000;++i) res[i][i]=0;
	cin>>len>>m>>s>>t;
	for(int i=1;i<=m;++i) cin>>w[i]>>u[i]>>v[i],pos[++n]=u[i],pos[++n]=v[i];
	sort(pos+1,pos+n+1);
	n=unique(pos+1,pos+n+1)-(pos+1);
	s=lower_bound(pos+1,pos+n+1,s)-pos;
	t=lower_bound(pos+1,pos+n+1,t)-pos; //大型离散化 
	for(int i=1;i<=m;++i){
		u[i]=lower_bound(pos+1,pos+n+1,u[i])-pos;
		v[i]=lower_bound(pos+1,pos+n+1,v[i])-pos;
		edge[u[i]][v[i]]=w[i];
		edge[v[i]][u[i]]=w[i];
	}
	cout<<qpow(len);
	return 0;
}

P2172 [国家集训队] 部落战争

这个题要简单一些,首先是要拆点一个连源点一个连汇点,然后一遍 O(nm) 枚举判断向下四个方向能不能站人,能站人就建边,然后跑最大流,答案就是城镇总数减最大流。

还有就是点的编号珂以用公式也珂以直接输入的时候就记录下来,以及这个能到达的点的编号不要弄错。

顺便,记得做P2764P2765,这三道题都是DAG的最小路径覆盖问题,只是处理细节和建边方式不同罢了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145149191810;
ll s,t,ans;
struct xx{
	ll next,to,val;
}e[2*N];
ll head[2*N],cur[2*N],cnt=1;
void add(ll x,ll y,ll z){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	e[cnt].val=z;
	head[x]=cnt;
	e[++cnt].next=head[y];
	e[cnt].to=x;
	e[cnt].val=0;
	head[y]=cnt;
}
ll dept[N];
bool spfa(){
	queue <ll> q; q.push(s);
	memset(dept,0,sizeof(dept));
	dept[s]=1; cur[s]=head[s];
	while(!q.empty()){
		ll u=q.front(); q.pop();
		for(int i=head[u];i;i=e[i].next){
			ll v=e[i].to,w=e[i].val;
			if(!dept[v]&&w){
				q.push(v);
				dept[v]=dept[u]+1;
				cur[v]=head[v];
			}
		}
	}
	return dept[t];
}
ll dfs(ll u,ll val){
	if(u==t) return val;
	ll use=0;
	for(int i=cur[u];i&&use<val;i=e[i].next){
		ll v=e[i].to,w=e[i].val;
		if(dept[v]==dept[u]+1&&w){
			ll res=dfs(v,min(w,val-use));
			if(!res) dept[v]=-1;
			e[i].val-=res,e[i^1].val+=res;
			val-=res,use+=res;
		}
	}
	return use;
}
ll n,m,r,c,id[55][55],tot,sum;
bool f[55][55]; char ch;
int main(){
	cin>>n>>m>>r>>c; sum=n*m;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j){
			cin>>ch,f[i][j]=(ch=='x');
			id[i][j]=++tot;
			if(f[i][j]) --sum;
		}
	s=0,t=tot*2+1;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j){
			if(f[i][j]) continue;
			add(s,id[i][j],1),add(id[i][j]+n*m,t,1);
			if(i+r<=n&&j+c<=m&&!f[i+r][j+c]) add(id[i][j],id[i+r][j+c]+n*m,1);
			if(i+c<=n&&j+r<=m&&!f[i+c][j+r]) add(id[i][j],id[i+c][j+r]+n*m,1);
			if(i+r<=n&&j-c>=1&&!f[i+r][j-c]) add(id[i][j],id[i+r][j-c]+n*m,1);
			if(i+c<=n&&j-r>=1&&!f[i+c][j-r]) add(id[i][j],id[i+c][j-r]+n*m,1);
		}
	while(spfa()){
		ll x;
		while((x=dfs(s,inf))) ans+=x;
	}
	cout<<sum-ans;
	return 0;
}

P2417 课程

据说数据很水,用一个极不正确且能被轻松HACK的贪心水过,当然这样就没意思了。我看还珂以直接用Dinic搞最大匹配,也确实行得通,但是有一种更好的贪心(虽然我也不知道会不会被HACK)

首先我们由教室向学生连有向边,同时使学生的入度加一,注意学生的编号要加 m 避免和教室的编号冲突。然后根据所连学生人数对教室从小到大排序,然后依次为起点遍历,寻找未被标记的入度数最大的学生并且标记表示被这个教室占了,如果占不到了那么就输出NO,最后都有匹配就输出YES。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145141919810;
ll T;
ll m,n,du[N],ans[N];
struct gx{
	ll id,k;
}a[N];
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
bool cmp(gx x,gx y){
	return x.k<y.k;
}
void solve(){
	memset(head,0,sizeof(head)); cnt=0;
	memset(du,0,sizeof(du));
	memset(ans,0,sizeof(ans));
	cin>>m>>n;
	for(int i=1;i<=m;++i){
		cin>>a[i].k; a[i].id=i;
		ll x;
		for(int j=1;j<=a[i].k;++j){
			cin>>x,add(i,m+x); //避免编号冲突
			++du[m+x];
		}
	}
	sort(a+1,a+m+1,cmp); //这里排序是到m !!!
	for(int j=1;j<=m;++j){
		ll maxn=inf,id=-1;
		for(int i=head[a[j].id];i;i=e[i].next){
			ll v=e[i].to;
			if(du[v]<maxn&&!ans[v]){
				maxn=du[v];
				id=v;
			}
		}
		if(id==-1){
			cout<<"NO\n";
			return;
		}
		ans[id]=1;
	}
	cout<<"YES\n";
}
int main(){
	cin>>T;
	while(T--) solve();
	return 0;
}

P2024 [NOI2001] 食物链

一眼冰茶姬,乱搞20pts,正解种类冰茶姬,开三倍空间,每一段 n 存A/B/C。然后并查集合并及判断的时候对应着来,如果是 xy 那么就把A中的 x 和B中的 y 合并,而其他关系都可以通过“XX吃了XX”这一关系推导出来。

先放这儿,有点不知道怎么说反正感觉不好……

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145140,M=1919810,inf=1145141919810;
ll n,k,f[3*N],ans=0,tot;
//种类冰茶姬……开三倍空间存ABC
//和关押罪犯略有不同 
ll find(ll x){
	return x==f[x]?x:f[x]=find(f[x]);
}
int main(){
	//freopen("Aha.in","r",stdin);
	//freopen("Aha.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=3*n;++i) f[i]=i;
	tot=n;
	for(int i=1;i<=k;++i){
		ll opt,x,y,u,v;
		cin>>opt>>u>>v;
		if(u>n||v>n){
			++ans;
			continue;	
		}
		if(opt==1){
			if(find(u+n)==find(v)||find(u)==find(v+n)) ++ans;
			else{
				f[find(u)]=find(v);
				f[find(u+n)]=find(v+n);
				f[find(u+2*n)]=find(v+2*n);
			}
		}
		else{
			if(find(u)==find(v)||find(u)==find(v+n)) ++ans;
			else{
				f[find(u)]=find(v+2*n);
				f[find(u+n)]=find(v);
				f[find(u+2*n)]=find(v+n);
			}
		}
	}
	cout<<ans;
	return 0;
}

P2474 [SCOI2008] 天平

还以为是一个巨大的分类讨论,结果正解是一个很简洁的差分约束。记录每对点之间的最大最小差值 maxni,j,minni,j,然后跑一遍Floyd求出来。最后分类讨论一下满足三种情况的条件就好了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114,M=1919810;
ll n,a,b;
ll maxn[N][N],minn[N][N];
ll ans1,ans2,ans3;
int main(){
	cin>>n;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j){
			char ch; cin>>ch;
			if(i==j||ch=='=') maxn[i][j]=minn[i][j]=0;
			if(ch=='+') maxn[i][j]=2,minn[i][j]=1;
			if(ch=='-') maxn[i][j]=-1,minn[i][j]=-2;
			if(ch=='?'&&i!=j) maxn[i][j]=2,minn[i][j]=-2;
		}
	for(int k=1;k<=n;++k) //注意Floyd应该这样写
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
				maxn[i][j]=min(maxn[i][j],maxn[i][k]+maxn[k][j]),
				minn[i][j]=max(minn[i][j],minn[i][k]+minn[k][j]);
	cin>>a>>b;
	for(int x=1;x<=n;++x)
		for(int y=1;y<x;++y){
			if(x==a||y==a||x==b||y==b) continue;
			if(minn[a][x]>maxn[y][b]||minn[a][y]>maxn[x][b]) ++ans1;
			if(minn[a][x]==maxn[a][x]&&maxn[a][x]==maxn[y][b]&&maxn[y][b]==minn[y][b]) ++ans2;
			else if(minn[b][x]==maxn[b][x]&&maxn[b][x]==maxn[y][a]&&maxn[y][a]==minn[y][a]) ++ans2;
			if(maxn[a][x]<minn[y][b]||maxn[a][y]<minn[x][b]) ++ans3;
		}
	cout<<ans1<<" "<<ans2<<" "<<ans3;
	return 0;
}

和树有关的但不涉及其他算法就放这里来吧。

P5658 [CSP-S2019] 括号树

考虑从链的部分分入手,我们可以把每个括号先匹配了,然后可以发现每一对匹配的括号的贡献(我们初始设成 1)等于他的上一对匹配括号的贡献加一,那么这样递推着来搞就行了。

部分code

点击查看代码
for(int i=1;i<=n;++i){
		if(f[i]) st.push(i);
		else if(st.size()){
			p[st.top()]=i; //记录配对括号
			p[i]=st.top();
			val[i]=1; //记录贡献
			st.pop();
		}
	}
	for(int i=1;i<=n;++i)
		if(p[i]&&!f[i])
			if(p[p[i]-1]&&!f[p[i]-1])
				val[i]=val[p[i]-1]+1;
	for(int i=1;i<=n;++i) val[i]+=val[i-1];
	for(int i=1;i<=n;++i) ans=ans^(i*val[i]);

接下来怎么把环转成树并且在 O(nlogn) 的时间内完成。我们发现链中关于每对括号贡献的结论依然有用,换成了从点的父亲转移过来,问题是怎么找到上一对匹配括号。

我们一般在用栈记录括号时每次递归都会插入再弹出,但是这样可能就会破坏版本的信息,所以我们考虑复原版本,每次有信息弹出就把他加回去(当前点递归完成后),然后像链一样计算贡献就行了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=5*114514,M=1919810,inf=1145141919810114514;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,x,f[N],ans,sum[N];
ll val[N],fat[N];
ll s[N],top;
void dfs(ll u,ll fa){
	ll res=0;
	if(f[u]) s[++top]=u;
	else{
		if(top){
			res=s[top];
			val[u]=val[fat[res]]+1;
			--top;
		}
	}
	sum[u]=sum[fat[u]]+val[u];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
	}
	if(res) s[++top]=res;
	else if(top) --top;
}
int main(){
	//ios::sync_with_stdio(0);
	//cin.tie(0); cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i){
		char ch; cin>>ch;
		f[i]=(ch=='(');
	}
	for(int i=1;i<n;++i){
		cin>>x;
		add(i+1,x),add(x,i+1);
		fat[i+1]=x;
	}
	dfs(1,0);
	for(int i=1;i<=n;++i) ans=ans^(i*sum[i]);
	cout<<ans;
	return 0;
}

P5666 [CSP-S2019] 树的重心

我们可以先发掘几条性质:

  1. 一棵树的重心有一个或两个,若有两个他们必定相邻。我们在下面求出的重心一定是深度较大的那一个。

  2. 一棵树的重心必定在以这个树根为起点的重链上。

  3. 一棵树的重心必定是以/这棵树的根的重儿子/为根/的子树/的重心/的祖先(有点绕,但确是如此)

那我们就珂以把重心当作树的根,根据性质3预处理出所有子树的重心,从叶往根处理,时间 O(n)

每当我们删去一条边后,树会分成两个部分,设深度小的点为 x,深度大的点为 yy 部分的答案就是预处理出来的重心,x 部分的答案需要分两类:

  1. y 不在根的重儿子子树内:这样子我们根据性质2就可以沿着根的重链,根据子树的大小去把答案预处理出来,也是 O(n)

  2. y 在根的重儿子的子数内:我们可以照着上一种情况把在根的次重儿子所在的重链的重心预处理出来。

然后在统计答案的时候还要判断我们求出来的重心的父亲是不是也是重心,根据是哪一种情况来变化。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3*114514,M=1919810;
ll T,n,a[N],b[N];
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll size[N],son[N],f[N],dept[N],son_t[N]; //属于根的哪个子树 
ll root,leson; //整棵树重心和根的次重儿子 
void dfs_pre(ll u,ll fa){
	size[u]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs_pre(v,u);
		size[u]+=size[v];
		if(size[son[u]]<size[v]) son[u]=v;
	}
	if(size[son[u]]*2<=n&&(n-size[u])*2<=n) root=u;
}
void dfs_son(ll u,ll fa){
	size[u]=1; dept[u]=dept[fa]+1; f[u]=fa;
	if(fa==root) son_t[u]=u;
	else son_t[u]=son_t[fa];
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dfs_son(v,u);
		size[u]+=size[v];
		if(size[son[u]]<size[v]){
			if(u==root) leson=son[u];
			son[u]=v;
		}
		else if(u==root&&size[v]>size[leson]) leson=v;
	}
}
ll ans1[N],ans2[N],ans3[N],tot;
void dfs_ans1(ll u,ll fa){ //直接求某子树的重心 
	if(son[u]) dfs_ans1(son[u],u);
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa||v==son[u]) continue;
		dfs_ans1(v,u);
	}
	ll now=son[u]?ans1[son[u]]:u; //从重心往上找
	while(size[now]*2<size[u]) now=f[now];
	ans1[u]=now; 
}
void dfs_ans2(ll u,ll fa){
	if(son[u]) dfs_ans2(son[u],u); //都走重儿子 
	while(n-2*size[u]<=tot&&tot>0){
		ans2[tot]=u;
		--tot;
	}
}
void dfs_ans3(ll u,ll fa){
	if(u==root) tot=n,dfs_ans3(leson,u); //根节点走次重儿子 
	else if(son[u]) dfs_ans3(son[u],u); //其他走重儿子
	while(n-2*size[u]<=tot&&tot>0){
		ans3[tot]=u;
		--tot;
	}
}
//三个ck判断是否有两个重心 
bool ck1(ll x,ll y){ //第一种他的父亲是不是重心 
	return x&&size[son[x]]*2<=size[y]&&(size[y]-size[x])*2<=size[y];
}
bool ck2(ll x,ll y){ //第二种父亲是不是重心 
	if(x==root){
		if(size[son[x]]*2<=n-size[y]) return 1;
		return 0; 
	}
	return x&&size[son[x]]*2<=n-size[y]&&(n-size[x]-size[y])*2<=n-size[y];
} 
bool ck3(ll x,ll y){ //第三种父亲是不是重心 
	if(x==root){
		if(size[leson]*2<=n-size[y]) return 1;
		//判的是次重儿子 
		else return 0;
	}
	return x&&size[son[x]]*2<=n-size[y]&&(n-size[x]-size[y])*2<=n-size[y];
}
void solve(){
	cin>>n;
	for(int i=1;i<=2*n;++i) head[i]=0,son[i]=0;
	tot=n,cnt=leson=0;
	for(int i=1;i<n;++i){
		cin>>a[i]>>b[i];
		add(a[i],b[i]),add(b[i],a[i]);
	}
	dfs_pre(1,0);
	for(int i=1;i<=n;++i) son[i]=0;
	dfs_son(root,0);
	dfs_ans1(root,0);
	dfs_ans2(root,0);
	dfs_ans3(root,0);
	ll ans=0;
	for(int i=1;i<n;++i){
		ll x=a[i],y=b[i];
		if(dept[x]>dept[y]) swap(x,y);
		ll pos1=ans1[y];
		ans+=pos1;
		if(dept[f[pos1]]>=dept[y]&&ck1(f[pos1],y)) ans+=f[pos1];
		if(son_t[y]!=son[root]){ //不在根的重儿子子树内
			ll pos2=ans2[size[y]];
			ans+=pos2;
			if(ck2(f[pos2],y)) ans+=f[pos2];
		}
		else{
			if(size[son[root]]-size[y]>=size[leson]) ans+=root; //根还是为重心
			else{
				ll pos3=ans3[size[y]];
				ans+=pos3;
				if(ck3(f[pos3],y)) ans+=f[pos3];
			} 
		}
	}
	cout<<ans<<'\n';
}
int main(){
	cin>>T;
	while(T--) solve();
	return 0;
}

E. Famil Door and Roads

并不是dp,虽然预处理有一点换根的感觉。

先预处理一些东西,dis 表示当前点到他字数内的点的距离之和,dis2 表示当前点到其他所有点的距离之和,两遍dfs搞定。

=,然后考虑不同的情况。设深度较大的点为 a,另一个为 b
1.a,b 没有祖先关系,这个时候环数为 num=size[a]size[b],总长度为 lennum+dis[a]size[b]+dis[b]size[a]len 两点间路径长度)。
2.a,b 有祖先关系,我们画张图表示,其中 ca 的祖先也是 b 的直系儿子,x,y 为可选的点:把  换一下a,b 换一下
那么环数就是 size[a](nsize[c]),总长度就为 lennum+size[a](dis2[b]dis[c]size[c])+dis[a](nsize[c]),注意 dis2[b] 要多减去一个 size[c],根据 dis2 的定义你是多给 c 的子树的所有点的距离加了一的。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810;
struct xx{
	ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
ll n,m,dept[N],size[N],dis[N],dis2[N];
ll f[N][26],lg[N];
void dfs_pre(ll u,ll fa){
	size[u]=1;
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dept[v]=dept[u]+1;
		f[v][0]=u;
		for(int j=1;j<=lg[dept[v]];++j)
			f[v][j]=f[f[v][j-1]][j-1];
		dfs_pre(v,u);
		size[u]+=size[v];
		dis[u]+=dis[v]+size[v];
	}
}
void dfs_pre2(ll u,ll fa){
	for(int i=head[u];i;i=e[i].next){
		ll v=e[i].to;
		if(v==fa) continue;
		dis2[v]=dis2[u]-size[v]+(n-size[v]);
		dfs_pre2(v,u);
	}
}
ll query_lca(ll a,ll b){
	if(a==b) return a;
	if(dept[a]<dept[b]) swap(a,b);
	for(int i=lg[dept[a]];i>=0;--i)
		if(dept[f[a][i]]>=dept[b])
			a=f[a][i];
	if(a==b) return a;
	for(int i=lg[dept[a]];i>=0;--i)
		if(f[a][i]!=f[b][i]){
			a=f[a][i];
			b=f[b][i];
		}
	return f[a][0];
}
ll query(ll a,ll b){
	for(int i=lg[dept[a]];i>=0;--i)
		if(dept[f[a][i]]>dept[b])
			a=f[a][i];
	return a;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<n;++i){
		ll a,b;
		cin>>a>>b;
		add(a,b),add(b,a);
	}
	for(int i=2;i<=n;++i) lg[i]=lg[i>>1]+1;
	dept[1]=1,dfs_pre(1,0);
	for(int i=1;i<=n;++i) dis2[1]+=dept[i]-1; //算距离要-1
	dfs_pre2(1,0);
	for(int i=1;i<=m;++i){
		ll a,b,c;
		cin>>a>>b; c=query_lca(a,b);
		if(a==b){
			cout<<"0.00000000\n";
			continue;
		}
		if(dept[a]<dept[b]) swap(a,b);
		ll sum=0,num=0;
		//分类讨论
		if(a!=c&&b!=c){
			num=size[a]*size[b];
			sum+=(dept[a]+dept[b]-2*dept[c]+1)*num; //两点间路径
			sum+=dis[a]*size[b]+dis[b]*size[a];
			printf("%0.8lf\n",double(sum*1.0/num));
		}
		else{
			ll c=query(a,b);
			num=size[a]*(n-size[c]);
			sum+=num*(dept[a]-dept[b]+1);
			sum+=size[a]*(dis2[b]-dis[c]-size[c])+dis[a]*(n-size[c]);
			printf("%0.8lf\n",double(sum*1.0/num));
		}
	}
	return 0;
}

P6880 [JOI 2020 Final] オリンピックバス

重在转化。
首先考虑暴力,每次删了边后直接重跑dijkstraO(n2m) 完全过不了,于是我们考虑怎么来减少跑dij的次数。想到有一些边即使反转了它也不会对答案造成影响,所以我们可以把所有边分为反转后会影响答案的“关键边”和“非关键边”,显然对于非关键边我们就不需要重新跑一遍最短路了。

求出这些非关键边也比较简单,直接在一开始跑 1 n,n 1 的最短路的时候标记一下就行。然后是怎么求这个边翻转过来之后的最短路,重新建图肯定不明智(这题还有重边)。我们考虑提前就预处理出把所有边都反过来时 1 n,n 1 的最短路,然后在查询的时候简单分类讨论一下,是直接从 1 n 的最短路短还是经过反转的边的最短路短(当然这两者可能相同),就是别忘了加上当前边的边权和反转费用。

一些细节:这个题用结构体封装起来会方便一些,但是我人傻没用;还要开读入优化要不然TLE;这个题 nm 大是个稠密图,用 dijkstra 就不需要堆优化了,直接每次 n2 就行了,意思就是不能用队列;注意存下非关键边的通用答案。
以上。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145141919810;
struct edge{
	ll to,val,wal,id;
};
vector <edge> g[N],f[N];
ll n,m,dis1[N],dis2[N],dis3[N],dis4[N];
ll di1[N],di2[N],di3[N],di4[N];
ll fro1[N],fro2[N],fro3[N],fro4[N]; //上一个点 
bool fl1[N],fl2[N],fl3[N],fl4[N],vis[N];
ll U[N],V[N],W[N],D[N],ans;
//先当作没有当前边跑,再建反向边跑
void dij(ll st,ll *dis,vector <edge> *g,ll tar,bool *fl,ll *fro,ll *di){
	for(int i=0;i<=n;++i) dis[i]=inf,vis[i]=0;
	dis[st]=0;
	for(int i=1;i<=n;++i){
		//仔细想想也确实是 
		ll u=0;
		for(int j=1;j<=n;++j)
			if(!vis[j]&&dis[j]<dis[u])
				u=j;
		if(!u) break;
		vis[u]=1;
		for(edge j:g[u]){
			ll v=j.to,id=j.id,w=j.val;
			if(id==tar) continue;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				q.push(v);
				if(!tar){
					fl[fro[v]]=0;
					fro[v]=id;
					fl[id]=1; //标记关键边 
				}
			}
		}
	}
	if(!tar) for(int i=1;i<=n;++i) di[i]=dis[i];
}
ll query1(ll s,ll t,ll id){
	if(s==1){
		if(!fl1[id]) return di1[t]; //不是关键边就不跑了
		dij(s,dis1,g,id,fl1,fro1,di1);
		return dis1[t];
	}
	else{
		if(!fl2[id]) return di2[t]; //不是关键边就不跑了
		dij(s,dis2,g,id,fl2,fro2,di2);
		return dis2[t];
	}
}
ll query2(ll s,ll t,ll id){
	if(s==1){
		if(!fl3[id]) return di3[t]; //不是关键边就不跑了
		dij(s,dis3,f,id,fl3,fro3,di3);
		return dis3[t];
	}
	else{
		if(!fl4[id]) return di4[t]; //不是关键边就不跑了
		dij(s,dis4,f,id,fl4,fro4,di4);
		return dis4[t];
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;++i){
		cin>>U[i]>>V[i]>>W[i]>>D[i];
		g[U[i]].push_back((edge){V[i],W[i],D[i],i});
		f[V[i]].push_back((edge){U[i],W[i],D[i],i});
	}
	dij(1,dis1,g,0,fl1,fro1,di1);
	dij(n,dis2,g,0,fl2,fro2,di2);
	dij(1,dis3,f,0,fl3,fro3,di3);
	dij(n,dis4,f,0,fl4,fro4,di4);
	//1~n,n~1/反转后的1~n,n~1
	ans=dis1[n]+dis2[1]; //关键边并不多
	for(int i=1;i<=m;++i)
		ans=min(ans,min(query1(1,n,i),query1(1,V[i],i)+query2(n,U[i],i)+W[i])+min(query1(n,1,i),query1(n,V[i],i)+query2(1,U[i],i)+W[i])+D[i]);
	if(ans<inf) cout<<ans;
	else cout<<-1;
	return 0;
}
posted @   和蜀玩  阅读(16)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示