2020暑假牛客多校训练营第五场(B/D)

文章目录

  • B.Graph
  • D. Drop Voicing

B.Graph

题目描述
给一棵树,每条边存在边权,可以对任意两个本身不连接的点相连,这条边的权值和所形成的环异或和为0。问在保持所有点连通的情况下,最后求最小边权之和。
思路
mark一下:Boruvak算法
加了几条边模拟一下之后发现两点之间边的权值根本不会发生改变,那么就可以把问题转化成给 n n n个点赋值,点与点的边权为两点的异或值,问将所有点连通的最小边权之和是多少。这个题转化一下就变成了异或最小生成树,cf也有原题。
求异或最小生成树一开始的想法把自己假了,我从点权最大的值开始从大到小找,同时维护一棵动态的Trie树,后来把自己写傻了emmm。
先把所有值sort一遍,贪心的思考,想要让异或和最小,就要让尽可能多的位数上的数字相同,对于一棵Trie树,可以自底向上跑,把每一个结点的左子树和右子树合并,左子树和右子树中选择的点一定是异或和最小的两个点,这一步操作其实就是对两个连通块的连通操作,而且在更新当前两个子树时,底部一定已经连通了。
码的时候其实有些小细节没考虑到位。在每次清空Trie树的时候,需要把0号点也置为0。
代码

#include
using namespace std;

typedef long long LL;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 10;
int tr[N * 30][2], idx;
struct edge {
    int v, w;
};
vector<edge> e[N];
int a[N];
LL res = 0;

// 将边权转换成点权
void dfs1(int u, int fa) {
    for(int i = 0; i < e[u].size(); i++) {
        int v = e[u][i].v, w = e[u][i].w;
        if(v == fa) continue;
        a[v] = a[u] ^ w;
        dfs1(v, u);
    }
}

void insert(int num) {
    int p = 0;
    for(int i = 29; i >= 0; i--) {
        int x = (num >> i) & 1;
        if(!tr[p][x]) tr[p][x] = ++idx;
        p = tr[p][x];
    }
}

int query(int num) {
    int res = 0;
    int p = 0;
    for(int i = 29; i >= 0; i--) {
        int x = (num >> i) & 1;
        if(tr[p][x]) {
            p = tr[p][x];
        }
        else {
            p = tr[p][!x];
            res += (1 << i);
        }
    }
    return res;
}

void dfs(int l, int r, int depth) {
    if(depth == -1 || l >= r) return;
    int mid = l - 1;
    while(mid < r && ((a[mid + 1] >> depth) & 1) == 0) mid++;

    dfs(l, mid, depth - 1);
    dfs(mid + 1, r, depth - 1);
    if(mid == l - 1 || mid == r) return;
    for(int i = l; i <= mid; i++) {
        insert(a[i]);
    }
    int num = inf;
    for(int i = mid + 1; i <= r; i++) {
        num = min(num, query(a[i]));
    }
    res += (LL)num;
    for(int i = 0; i <= idx; i++) {
        tr[i][1] = tr[i][0] = 0;
    }
    idx = 0;
}

void solve() {
    int n;
    scanf("%d", &n);
    for(int i = 1; i < n; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        u++, v++;
        e[u].push_back({v, w});
        e[v].push_back({u, w});
    }
    a[1] = 0;
    dfs1(1, 0);
    sort(a + 1, a + 1 + n);
    dfs(1, n, 29);
    printf("%lld\n", res);
}

int main() {
//    freopen("in.txt", "r", stdin);
    solve();
    return 0;
}

D. Drop Voicing

题目描述
对于一个随机的排列 P P P,有两种操作:
1、将第一个数挪到最后一位上。
2、将倒数第二位挪到第一位上。
连续的相同操作仅看成一次操作。最少需要几次第二种操作可以把序列变成一个 1 1 1~ n n n排列的有序序列。
思路
把这个排列看成一个环,第一种操作其实就是对于这个环旋转一下,第二种操作其实就是挑一个数,将这个数逆时针挪动 x x x位。两个操作一结合其实就是把一个数任意的挪到任何一个位置。那么要求最少的第二次操作,就求一下原排列可能的 n n n种的最长上升子序列即可。
代码

#include
using namespace std;

const int inf = 0x3f3f3f3f;
const int N = 510;
int a[N << 1];
int dp[N << 1];

int cacl(int l, int r) {
	memset(dp, 0, sizeof dp);
	dp[l] = -inf;
	int len = 0;
	for(int i = l; i <= r; i++) {
		int l = 0, r = len;
		int id = -1;
		int num = a[i];
		while(l <= r) {
			int mid = l + r >> 1;
			if(dp[mid] >= num) {
				id = mid;
				r = mid - 1;
			} else l = mid + 1;
		}
		if(id == -1) id = ++len;
		dp[id] = num;
	}
	return len;
}

void solve() {
	int n;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		a[i + n] = a[i];
	}
	int res = 1;
	for(int i = 1; i <= n; i++) {
		res = max(res, cacl(i, i + n - 1));
	}
	printf("%d\n", n - res);
}

int main() {
	// freopen("in.txt", "r", stdin);
	solve();
	return 0;
}

你可能感兴趣的:(2020暑假牛客多校训练营第五场(B/D))