JZOJ 6997. 2021.03.06【2021省赛模拟】排列(最小树形图)

JZOJ 6997. 2021.03.06【2021省赛模拟】排列

题目大意

  • p p p为一个 1 1 1 n n n的排列,令 F ( p ) = ∑ i = 1 n min ⁡ j = 1 i a p i ⨁ b p j F(p)=\sum_{i=1}^n \min_{j=1}^i a_{p_i}\bigoplus b_{p_j} F(p)=i=1nminj=1iapibpj,求使 F ( p ) F(p) F(p)最小且字典序最小的 p p p
  • n ≤ 50 n\le50 n50

题解

  • 若选择 b y b_y by a x a_x ax构成一组贡献,则相当于从 y y y x x x连了一条边,即要求 y y y在排列中需要在 x x x的前面,把所有的 ( x , y ) (x,y) (x,y)对应的边都连出来,边权为 b y ⨁ a x b_y\bigoplus a_x byax,则题目可以转化为在这些边中选出一些边构成树形结构,其中边的方向都由父亲指向儿子,并最小化边权和。
  • 基本上就是求最小树形图,但同时要考虑排列字典序最小。可以依次枚举每个位置填什么数,并固定下来,用剩下的点求最小树形图。每次选择权值和最小的且最小的数填入当前位置。
  • 在实现上有细节需要注意:最小树形图中,可能会出现自己连向自己的情况,并且也是合法的;用剩下的点求最小树形图时,根节点不仅是当前固定下来的点,而是前面已经固定好的所有点合并作为根。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 110
int a[N], b[N], dis[N][N], ar[N], tot, n;
int ch[N], st[N], fa[N], vi[N], e[N][N], sn[N][N];
int solve(int x) {
	int s = dis[fa[x]][x];
	int ci[N];
	ci[0] = 0;
	for(int i = 1; i <= tot; i++) if(sn[x][i]) ci[sn[x][i]] = i, ci[0]++;
	for(int i = 1; i <= ci[0]; i++) fa[ci[i]] = ci[i % ci[0] + 1], s += solve(ci[i]);
	return s;
}
int count(int v0) {
	int i, j, k, ok = 0, l = -1;
	memset(sn, 0, sizeof(sn));
	while(!ok) {
		l++;
		memset(fa, 0, sizeof(fa));
		memset(vi, 0, sizeof(vi));
		// 非根节点的贡献 
		for(i = 1; i <= e[l][0]; i++) if(e[l][i] != v0) {
			int mi = 1e9, p, x = e[l][i];
			// 在集合内,由非根、非自己转移 
			for(j = 1; j <= e[l][0]; j++) if(e[l][j] != x && dis[e[l][j]][x] < mi) mi = dis[e[l][j]][x], p = e[l][j];
			fa[x] = p;
			// 由自己、根或已确定的序列转移 
			int mii = (x <= n ? dis[x][x] : 1e9); p = x;
			for(j = 1; j <= ar[0]; j++) if(dis[ar[j]][x] < mii) mii = dis[ar[j]][x], p = ar[j];
			if(mii <= mi) {
				vi[x] = 1;
				fa[x] = p;
			}
		}
		// vi[] = 1 --> 挂入确定的点,不可能产生环。需计入下一层,可任意往后挂 
		vi[v0] = 1; int id = 1; 
		ok = 1;
		for(i = 1; i <= e[l][0]; i++) if(vi[e[l][i]]) e[l + 1][++e[l + 1][0]] = e[l][i];
		for(i = 1; i <= e[l][0]; i++) if(!vi[e[l][i]]) {
			id++;
			st[0] = 0;
			int x = e[l][i];
			while(!vi[x]) {
				vi[x] = id;
				st[++st[0]] = x;
				x = fa[x];
			}
			if(vi[x] == id) {
				//构成环,缩点 
				ok = 0;
				tot++;
				for(j = 1; j <= tot; j++) dis[tot][j] = dis[j][tot] = 1e9;
				for(j = 1; st[j] != x && j <= st[0]; j++);
				int j0 = j;
				for(j; j <= st[0]; j++) {
					int x = st[j];
					sn[tot][x] = j - j0 + 1;
					for(k = 1; k < tot; k++) {
						dis[tot][k] = min(dis[tot][k], dis[x][k]);
						dis[k][tot] = min(dis[k][tot], dis[k][x] - dis[fa[x]][x]);
					}
				}
				e[l + 1][++e[l + 1][0]] = tot;
				for(j = 1; st[j] != x && j <= st[0]; j++) {
					e[l + 1][++e[l + 1][0]] = st[j];
				}
			}
			else {
				//未构成环,加入下一层 
				for(j = 1; j <= st[0]; j++) e[l + 1][++e[l + 1][0]] = st[j];
			}
		}
	}
	int s = 0;
	for(i = 1; i <= e[l][0]; i++) {
		int x = e[l][i];
		s += solve(x); 
	}
	return s; 
}
int main() {
	int i, j, k;
	scanf("%d", &n);
	for(i = 1; i <= n; i++) scanf("%d", &a[i]);
	for(i = 1; i <= n; i++) scanf("%d", &b[i]);
	for(i = 1; i <= n; i++)
		for(j = 1; j <= n; j++) dis[i][j] = b[i] ^ a[j];
	for(i = 1; i <= n; i++) {
		int mi = 1e9, x;
		ar[0]++;
		for(j = 1; j <= n; j++) if(!ch[j]) {
			memset(e, 0, sizeof(e));
			for(k = 1; k <= n; k++) if(!ch[k]) e[0][++e[0][0]] = k;//未选加入集合 
			tot = n;
			ar[i] = j;	
			int t = count(j);
			// 当前根的贡献 
			int mii = 1e9;
			for(k = 1; k <= i; k++) mii = min(mii, dis[ar[k]][j]);
			if(t + mii < mi) mi = t + mii, x = j;
		}
		ch[x] = 1;
		ar[i] = x;
		if(i == 1) printf("%d\n", mi);
	}
	for(i = 1; i <= n; i++) printf("%d ", ar[i]);
	return 0;
}

自我小结

  • 这题看似是最小树形图的模板题,其实却有很多与之不同且极其需要注意的地方,同时是第一次写最小树形图,思路和细节上都出现了许多问题。
posted @ 2021-03-10 21:38  AnAn_119  阅读(49)  评论(0编辑  收藏  举报