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=1iapi⨁bpj,求使 F ( p ) F(p) F(p)最小且字典序最小的 p p p。
- n ≤ 50 n\le50 n≤50
题解
- 若选择 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 by⨁ax,则题目可以转化为在这些边中选出一些边构成树形结构,其中边的方向都由父亲指向儿子,并最小化边权和。
- 基本上就是求最小树形图,但同时要考虑排列字典序最小。可以依次枚举每个位置填什么数,并固定下来,用剩下的点求最小树形图。每次选择权值和最小的且最小的数填入当前位置。
- 在实现上有细节需要注意:最小树形图中,可能会出现自己连向自己的情况,并且也是合法的;用剩下的点求最小树形图时,根节点不仅是当前固定下来的点,而是前面已经固定好的所有点合并作为根。
代码
#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;
}
自我小结
- 这题看似是最小树形图的模板题,其实却有很多与之不同且极其需要注意的地方,同时是第一次写最小树形图,思路和细节上都出现了许多问题。
哈哈哈哈哈哈哈哈哈哈