「考试总结」2022-02-10


A.一般图带权多重匹配

暴力做法是枚举所有匹配找最小匹配边边权和最小值

在任意一组匹配图上跑欧拉回路都可以将其变成:奇数度数点出度入度差一,偶数度数点出度入度相同

那么枚举每个奇数度数点是出度大还是入度大,那么接下来可以使用费用流来计算最小匹配边权和

将每个点拆成出入点两个,出点向 \(T\) 连出度,入点向 \(S\) 连入度大小,出入点之间连矩阵中的边权即可

Code Display
const int N=210;
int a[N],n,cst[N][N];
const int M=N*N*100,inf=0x3f3f3f3f3f3f3f3f;
int dst[N],pre[N],S,T,incf[N],ans,head[N],cnt=1;
struct edge{int to,nxt,lim,cst;}e[M];
inline void adde(int u,int v,int w,int c){
	e[++cnt]={v,head[u],w,c}; head[u]=cnt;
	return ;
}
inline void add(int u,int v,int w,int c){return adde(u,v,w,c),adde(v,u,0,-c);}
queue q;
bool vis[N];
inline bool spfa(){
	rep(i,1,T) dst[i]=inf; dst[S]=0; incf[S]=inf; q.push(S);
	while(q.size()){
		int fr=q.front(); q.pop(); vis[fr]=0;
		for(int i=head[fr];i;i=e[i].nxt) if(e[i].lim){
			int t=e[i].to; if(e[i].cst+dst[fr]>=dst[t]) continue;
			pre[t]=i; dst[t]=dst[fr]+e[i].cst; incf[t]=min(incf[fr],e[i].lim);
			if(!vis[t]) q.push(t),vis[t]=1;
		}
	}
	return dst[T]!=inf;
}
inline int solve(){
	int ans=0;
	int sumflow=0;
	while(spfa()){
		int x=T;
		while(x^S){
			e[pre[x]].lim-=incf[T];
			e[pre[x]^1].lim+=incf[T];
			x=e[pre[x]^1].to;
		}
		sumflow+=incf[T];
		ans+=incf[T]*dst[T];
	}
	return ans;
}
vector ids;
signed main(){
	freopen("match.in","r",stdin); freopen("match.out","w",stdout);
	n=read();
	for(int i=1;i<=n;++i){
		a[i]=read();
		if(a[i]&1) ids.push_back(i);
	}
	if(ids.size()&1) puts("-1"),exit(0);
	rep(i,1,n) rep(j,1,n) cst[i][j]=read();
	int ans=inf;
	int U=1<>i&1) add(ids[i]+n,T,a[ids[i]]/2+1,0),add(S,ids[i],a[ids[i]]/2,0);
			else add(ids[i]+n,T,a[ids[i]]/2,0),add(S,ids[i],a[ids[i]]/2+1,0);
		}
		for(int i=1;i<=n;++i) if(a[i]%2==0){
			add(S,i,a[i]/2,0);
			add(i+n,T,a[i]/2,0);
		}
		rep(i,1,n) rep(j,1,n) add(i,j+n,inf,min(cst[i][j],cst[j][i]));
		ckmin(ans,solve());
		
	}
	print(ans);
	return 0;
}

B.排序

不难发现在求解过程中,对于每个左端点我们只关注其右端点最小值,同时如果两个线段存在包含关系那么较长者必然是不优的

那么使用 std::vector来进行线段的存储,每个子树的根处合并得到新的线段,对于每个左端点进行状压 \(\rm dp\)

\(f_S\) 表示当前左端点加入 \(S\) 集合中儿子节点后最小右端点的值和 \(g_S\) 表示最小右端点值对应的方式儿子的顺序

转移就是枚举新添加哪个儿子并在该儿子维护的线段集合中找到满足左端点限制的最小右端点即可,另外由于儿子的数量少于 \(10\),所以可以十进制压位存储

在计算完每个节点所有可行左端点对应的最小右端点之后要记得剥离包含关系来进行方案优化

如果某个节点没有合法线段则无解

复杂度上届是所有节点可行的左端点数量乘 \(\log+k2^{k-1}\),剥离包含关系是最有力的剪枝,下设重儿子是指 \(\sum m_i\) 最大的儿子

考虑每个轻儿子开始的左端点都可能对应一个右端点,而重儿子的左端点最多匹配轻儿子 \(\sum m_i\) 个右端点,故线段总量不超过 \(2\times\) 轻儿子的 \(\sum m\)

轻边继承的分析直接得到总量是 \(\Theta(n\log n)\) 级别的,外加合法性等判断实际上非常之松

注意上面的复杂度分析需要满足至少两个儿子,所以一个儿子的时候需要直接继承(swap vector)

Code Display
const int N=1e5+10,inf=0x3f3f3f3f;
struct Seg{int l,r,ord;}; 
vectorseg[N];
inline Seg get(int id,int pos){
	if(seg[id].back().l>1;
		if(seg[id][mid].l>=pos) ans=seg[id][mid],rig=mid-1;
		else lef=mid+1;
	} return ans;
} 
vector vals[N],G[N];
int f[2010],g[2010],n;
inline void dfs(int x){
	if(G[x].size()==0){
		sort(vals[x].begin(),vals[x].end());
		for(auto t:vals[x]) seg[x].push_back({t,t,-1});
		return ;
	}
	if(G[x].size()==1){
		dfs(G[x][0]);
		swap(seg[x],seg[G[x][0]]);
		return ;
	}
	int siz=G[x].size();
	int U=1<,int> >lef;
	for(int i=0;i>i&1)){
				Seg res=get(G[x][i],f[st]+1);
				if(res.r==-1) continue;
				int Nxt=(1<b.l;
		else return a.r New;
	int Mx=0;
	for(auto t:seg[x]){
		if(t.l<=Mx) continue;
		ckmax(Mx,t.l);
		New.push_back(t);
	}
	swap(New,seg[x]); 
	sort(seg[x].begin(),seg[x].end(),[](const Seg a,const Seg b){return a.l ans[N];
inline void get_ans(int x,Seg now){
	if(G[x].size()==0){
		ans[x].push_back(get(x,now.l).l);
		return ;
	}
	if(G[x].size()==1){
		swap(seg[x],seg[G[x][0]]);
		get_ans(G[x][0],now);
		ans[x]={G[x][0]};
		return ;
	}
	int lst=now.l-1;
	while(now.ord){
		int t=G[x][now.ord%10-1];
		ans[x].push_back(t);
		now.ord/=10;
	}
	reverse(ans[x].begin(),ans[x].end());
	for(auto t:ans[x]){
		Seg tmp=get(t,lst+1);
		get_ans(t,tmp);
		lst=tmp.r;
	}	
	return ;
}
signed main(){
	freopen("sort.in","r",stdin); freopen("sort.out","w",stdout);
	n=read();
	for(int i=1;i<=n;++i){
		if(read()-1){
			int m=read(); 
			while(m--) vals[i].push_back(read());
		}else{
			int m=read();
			while(m--) G[i].push_back(read());
		}
	}
	dfs(1);
	puts("Yes");
	get_ans(1,seg[1][0]);
	rep(i,1,n){
		for(auto val:ans[i]) print(val);
		puts("");
	}
	return 0;
}

C.传染

一种平方暴力就做法就是将每个点和它一步能覆盖的点连边,在新图上找到所有没有入度的强联通分量

所以观察这个做法本质上还是求没有入度的强联通分量,所以考虑维护一个删点序列,也就是在 \(\rm DAG\) 上后继点比前驱点出现的位置早(被从图上删去的时间早)

如果得到这个序列之后直接模拟感染/传染过程,倒序扫描序列,每次找最后没有被传染的点进行感染

由于感染每个点的代价是相同的,所以靠后的未传染点必然是属于没有入度强联通分量

不难发现上面说的序列和 \(\rm tarjan\) 算法本质是完全一样的,那么按照任一顺序模拟传染过程即可求出一组合法拓扑序

所以问题就是如何模拟传染过程,可以使用点分树实现,建树时维护子树中每个点到树根在原树上的距离 以及 每个点在点分树上和根链上每个点的距离 并排序

模拟传染过程时每个子树根维护一个指针表示指针之前的所有元素被感染过了,那么剩下的过程就是暴力跳了!

还有一种做法就是直接对着点分树前后缀优化建图,但是显然没有这种做法好写一些

Code Display
const int N=3e5+10;
int f[N],n,r[N],pter[N],siz[N],rt,nsum;
vector >sub[N],anc[N],g[N];
bool vis[N];
inline void findrt(int x,int fat){
	siz[x]=1; f[x]=0;
	for(auto e:g[x]){
		int t=e.fir; if(vis[t]||t==fat) continue;
		findrt(t,x); 
		ckmax(f[x],siz[t]); siz[x]+=siz[t];
	}
	ckmax(f[x],nsum-siz[x]);
	if(f[x]1e9) return ;
	sub[rt].emplace_back(dis,x);
	anc[x].emplace_back(dis,rt);
	for(auto e:g[x]) if(!vis[e.fir]&&e.fir!=fat) dfs(rt,e.fir,x,dis+e.sec);
	return ;
}
inline int resize(int x,int fat){
	int siz=1;
	for(auto e:g[x]) if(!vis[e.fir]&&e.fir!=fat) siz+=resize(e.fir,x);
	return siz;
}
inline void solve(int x){
	vis[x]=1;
	for(auto e:g[x]) if(!vis[e.fir]) dfs(x,e.fir,0,e.sec);
	anc[x].emplace_back(0,x);
	sub[x].emplace_back(0,x);
	sort(sub[x].begin(),sub[x].end());
	for(auto e:g[x]) if(!vis[e.fir]){
		rt=0;
		nsum=resize(e.fir,0);
		findrt(e.fir,0);
		solve(rt);
	}
	return ;
}
vector seq;
inline void cover(int x){
	vis[x]=1;
	for(auto t:anc[x]){
		int siz=sub[t.sec].size();
		while(pter[t.sec]r[x]){
				pter[t.sec]--;
				break;
			}
			cover(sub[t.sec][pter[t.sec]-1].sec);
		}
	}
	seq.push_back(x);
	return ;
}
signed main(){
	freopen("infect.in","r",stdin); freopen("infect.out","w",stdout);
	n=read(); f[0]=(nsum=n)+1;
	rep(i,1,n) r[i]=read();
	for(int i=1;i lst=seq;
	reverse(lst.begin(),lst.end());
	int ans=0;
	for(auto t:lst){
		if(vis[t]) continue;
		cover(t); 
		++ans;
	} 
	print(ans);
	return 0;
}