• AtCoder Grand Contest 009 简要题解


    AtCoder Grand Contest 009

    A - Multiple Array

    题解

    可以发现只有给 (1sim n) 这个前缀整体加一个数才能让 (a_n) 变成 (b_n) 的倍数。所以直接从后往前推就行了。

    实现

    #include <cstdio>
    using namespace std;
    const long long NN = 1e5 + 5;
    long long N, A[NN], B[NN];
    long long ad[NN];
    int main() {
    	freopen("a.in", "r", stdin);
    	freopen("a.out", "w", stdout);
    	scanf("%lld", &N);
    	for (long long i = 1; i <= N; ++i) {
    		scanf("%lld%lld", &A[i], &B[i]);
    		long long tmp = A[i] / B[i];
    		if (A[i] % B[i] != 0) //注意特判
    			ad[i] = (tmp + 1) * B[i] - A[i];
    	}
    	for (long long i = N - 1; i >= 1; --i) {
    		if (ad[i] < ad[i + 1])
    			ad[i] += (ad[i + 1] - ad[i] + B[i] - 1) / B[i] * B[i];
    	}
    	printf("%lld
    ", ad[1]);
    	fclose(stdin);
    	fclose(stdout);
    	return 0;
    }
    

    总结

    这种前缀的结构一把来说就是从后向前推,因为最后一个数只能被对前缀 (1sim n) 的操作改变。

    B - Tournament

    题解

    建一棵树,将第 (i) 个人击败的人当成 (i) 结点的儿子。题就相当于给每条边标一个编号,一条边代表一场比赛,编号表示在比赛的那个二叉树(题面上画的那棵树)上的深度。那么一个点 (u) 到儿子 (v) 的边的编号必然要比 (u)(u) 的父亲的边的编号大,而且任意两个 (u) 到儿子的边的编号不能相同。

    所以考虑设 (f_i) 表示 (i) 的子树内的最大编号比 (i) 到父亲的编号至少大多少,才能满足以上的分配条件。在处理出 (u) 节点的 (f) 值之前,需要将 (u) 的儿子的 (f) 值预先处理好。然后将 (u) 的儿子按照 (f) 值从大到小排序,然后给 (u) 的每个到儿子的边赋编号,将 (f) 值大的编号赋小一点,这样可以让 (u) 子树内编号最大的尽量小。如果排序后为 (s_1,s_2,cdots s_m) ,那么 (f_u=max_{i=1}^{m}(f_{s_i}+i)) 。这样一层一层向上递推就行了。

    实现

    #include <cstdio>
    #include <vector>
    #include <algorithm>
    using namespace std;
    const int NN = 1e5 + 5;
    int N, fa[NN], ans, trsz[NN], need[NN];
    vector<int> tr[NN];
    bool cmp(int i, int j) { return need[i] > need[j]; }
    void Dfs(int u) {
    	for (int i = 0; i < trsz[u]; ++i)
    		Dfs(tr[u][i]);
    	sort(tr[u].begin(), tr[u].end(), cmp); //忘记排序了
    	for (int i = 0; i < trsz[u]; ++i)
    		need[u] = max(need[u], need[tr[u][i]] + i + 1);
    }
    int main() {
    	freopen("b.in", "r", stdin);
    	freopen("b.out", "w", stdout);
    	scanf("%d", &N);
    	for (int i = 2; i <= N; ++i) {
    		scanf("%d", &fa[i]);
    		tr[fa[i]].push_back(i);
    	}
    	for (int i = 1; i <= N; ++i)
    		trsz[i] = tr[i].size();
    	Dfs(1);
    	printf("%d
    ", need[1]);
    	fclose(stdin);
    	fclose(stdout);
    	return 0;
    }
    //不是深度+度数的最大值
    //不是按照子树最大深度排序
    

    总结

    • 诸如按照子树的最大深度排序之类的贪心是错的。。
    • 大胆猜测,小心验证(开始乱猜答案为所有点深度+度数的最大值)

    这种递推还是很优美的。

    C - Division into Two

    题解

    (f_i) 表示最后一个数分在 (A) 集合中的方案数。

    那么 (f_i) 可以从 (f_j(j<i)) 转移当且仅当 (a_i-a_jge A) ,并且 (a_{j+1sim i}) 这些数可以都放到 (B) 中。但是是不是有可能 (a_{j+1})(a_{j-1}) 不能放在一块呢?如果不能放在一块,那么 (a_{j+1},a_{j-1},a_j) 两两不能放在一块,显然无解。所以先特判一些是否有解,然后直接 dp 就无论如何都能转移了。

    容易发现可以转移的是一段区间,前缀和优化转移即可。并不需要什么树状数组/线段树之类的乱七八糟数据结构。

    总结

    总是容易陷入误区诶。。。陷入诸如“设 (f_{i,j}) 表示 (A) 中最后一个数是 (a_i)(B) 中最后一个数是 (b_i) 的方案数” 的误区很常见欸。这样好像听说可以线段树优化?不过我不是很懂。

    实现

    #include <cstdio>
    #include <iostream>
    using namespace std;
    const int NN = 1e5 + 5, Mod = 1e9 + 7;
    int Ad(int x, int y) { return ((x + y) > Mod) ? (x + y - Mod) : (x + y); }
    int Dc(int x, int y) { return ((x - y) < 0) ? (x - y + Mod) : (x - y); }
    int N, f[NN], lft[NN], s[NN];
    long long A, B, a[NN];
    bool can[NN];
    int sum(int L, int R) {
    	if (L < 0 || R < 0) return 0;
    	if (L > R) return 0;
    	return Dc(s[R], s[L - 1]);
    }
    int main() {
    	scanf("%d%lld%lld", &N, &A, &B);
    	if (A < B) swap(A, B); //这道题的精髓所在
    	for (int i = 1; i <= N; ++i)
    		scanf("%lld", &a[i]);
    	for (int i = 2; i <= N - 1; ++i) {
    		if (a[i + 1] - a[i - 1] < B) {
    			printf("0
    ");
    			return 0;
    		}
    	}
    	for (int i = 2; i <= N; ++i) {
    		can[i] = ((a[i] - a[i - 1]) >= B);
    		if (can[i])
    			lft[i] = lft[i - 1];
    		else lft[i] = i;
    	}
    	int pt = 1;
    	s[0] = f[0] = 1;
    	for (int i = 1; i <= N; ++i) {
    		while (pt < i && a[i] - a[pt] >= A) ++pt;
    		//lft[i - 1] - 1 <= j <= pt - 1
    		f[i] = sum(max(lft[i - 1] - 1, 0), pt - 1);
    		s[i] = Ad(s[i - 1], f[i]);
    //		printf("%lld i : %d L : %d R : %d f[i] : %d
    ", a[i], i, max(lft[i - 1] - 1, 0), pt - 1, f[i]);
    	}
    	int ans = 0;
    	for (int i = N; i >= 0; --i) { //这里是 >=0
    		ans = Ad(ans, f[i]);
    		if (!can[i + 1] && i != N)
    			break ;
    	}
    	printf("%d
    ", ans);
    	return 0;
    }
    

    D - Uninity

    题解

    事实上题目等价于找出一种最优的指定分治中心的方法,使得点分树深度最小。

    设每个点在点分树上的深度为 (d_i) ,设 (a_i=M-d_i) (其中 (M) 为所有 (d_i) 的最大值)。那么这样的 (a_{1sim n}) 可以唯一对应一棵点分树。那么什么样的 (a_{1sim n}) 能对应出一棵点分树呢?容易发现树上任何两个点 (x)(y) 满足 (a_x=a_y) ,在 (x)(y) 的路径上必存在一个点满足 (a_z>a_x) 并且 (a_z>a_y) 。只要满足这一条件,那么每回必能选出每一连通块内的 (a_i) 最大的结点(重要的是,这个点必然唯一,因为任意两个相同权值的点路径上都会有比这个点权值大的点)并将它作为分治重心,然后可以将这个点去掉形成若干连通块并继续递归地进行下去。如果构建出满足这个条件(指大小关系这个条件)的数组 (b) 并微调就可以得到 (a) 数组。题目让你做的实际上就是让 (b) 数组的最大值最小。

    考虑将树以 1 为根节点,然后考虑给一个点标号。考虑这个点 (u) 不能标什么。对于 (u) 的子树内结点 (v) ,若 (v)(b) 值为到 (u) 的路径上的最大值,那么 (u) 就不能填 (v) 的值。如果 (u) 有两棵子树内有相同的 (b) 值,那么 (u) 必须填入比这个值大的数。在满足这些事情的条件下,贪心地考虑将 (u) 所填的编号尽量小即可。由于树的高度最多为 (log _2 n) 级别的,所以直接用一个 ( ext{bitmask}) 存储一个点不能填哪些数即可。复杂度 (mathcal O(nlog _2n)) 不过好像可以 (mathcal O(n)) ?我也不知道。

    总结

    想到要研究所有点的点分树上的深度,并找到什么样的 (a) 数组符合条件是很重要的一步。

    实现

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    const int NN = 1e5 + 5;
    int N, head[NN], totE;
    int mask[NN], ans;
    struct E {
    	int v, nxt;
    	E(int _v, int _nxt) : v(_v), nxt(_nxt) {}
    	E() {}
    } edge[NN * 2];
    void AddE(int u, int v) {
    	edge[++totE] = E(v, head[u]);
    	head[u] = totE;
    }
    int Get(int num, int wei) { return (num >> wei) & 1; }
    void Dfs(int u, int f) {
    	int cnt[20];
    	memset(cnt, 0, sizeof(cnt));
    	for (int p = head[u]; p; p = edge[p].nxt) {
    		int v = edge[p].v;
    		if (v != f) {
    			Dfs(v, u);
    			for (int i = 0; i < 17; ++i)
    				cnt[i] += Get(mask[v], i);
    			mask[u] |= mask[v];
    		}
    	}
    	int least = 0;
    	for (int i = 0; i < 17; ++i)
    		if (cnt[i] >= 2)
    			least = max(least, i + 1);
    	while (Get(mask[u], least)) ++least;
    	mask[u] >>= least;
    	mask[u] <<= least;
    	mask[u] |= (1 << least);
    	ans = max(ans, least);
    }
    int main() {
    	freopen("d.in", "r", stdin);
    	freopen("d.out", "w", stdout);
    	scanf("%d", &N);
    	for (int i = 1; i < N; ++i) {
    		int u, v;
    		scanf("%d%d", &u, &v);
    		AddE(u, v);
    		AddE(v, u);
    	}
    	Dfs(1, 0);
    	printf("%d
    ", ans);
    	fclose(stdin);
    	fclose(stdout);
    	return 0;
    }
    

    E - Eternal Average

    题解

    问题实际上相当于一棵 (k) 叉树,所有叶子结点都有一个 0 或 1 的数,每个非叶子结点上的数为叶子节点上的数的平均值。

    考虑根节点的值得表达式。其实就是 (displaystyle sum_{iin leaf}frac{val_i}{K^{d_i}}) ,其中 (leaf) 表示叶子集合,(val_i) 表示结点 (i) 上填的数。

    那么如果根节点的值为 (z) ,设 (z) 表示为 (K) 进制小数为 (0.c_1c_2cdots c_{len}) ,那么 (displaystyle sum_{i=1}^{len} c_iequiv Mpmod {K-1}) (感觉这里官方题解搞错了?(N,M) 可能他搞反了)。并且还需要 (displaystyle sum_{i=1}^{len} c_ile M)(1-z) 的数位和 (le N)(1-z) 的数位和为 (displaystyle (sum_{i=1}^{len-1}K-1-c_i)+K-c_{len}=Kcdot len-(len-1)-sum_{i=1}^{len}c_i=lencdot(K-1)+1-sum_{i=1}^{len}c_i) ,并且模 (K-1)(N) 同余。似乎还需要枚举一共有多少位(因为最后有多少位是 0 是不确定的,虽然总共的位数有限)。

    那么剩下的问题就是一个比较直接的 dp 可以解决的了。设 (displaystyle f_{i,j,0/1}) 表示前 (i) 位,总共用了 (j) 个数,最后一位是否为 0. 注意,小数最多可以有 (displaystyle frac{N+M-1}{K-1}) 位。转移是 (f_{i,j,0}=f_{i-1,j,0}+f_{i-1,j,1})(displaystyle f_{i,j,1}=left(sum_{k=1}^{min(K-1,j)}f_{i-1,j-k,0}+f_{i-1,j-k,1} ight)) .

    总结

    小数的分母都是 (k) 的次幂,所以要化为 (k) 进制。其他的静下心想还能想出来,(k) 进制也勉强能够想出来吧。关键是静下心想!

    实现

    #include <cstdio>
    #include <iostream>
    using namespace std;
    const int NN = 2e3 + 5, NM = 2e3 + 5, Mod = 1e9 + 7;
    inline int Ad(int x, int y) { return ((x + y) > Mod) ? (x + y - Mod) : (x + y); }
    inline int Dc(int x, int y) { return ((x - y) < 0) ? (x - y + Mod) : (x - y); }
    int N, M, K;
    int f[NN + NM][NM][2], s[NM];
    int Sum(int L, int R) {
    	if (L < 0 || R < 0 || R < L)
    		return 0;
    	return Dc(s[R], s[L - 1]);
    }
    int main() {
    	freopen("e.in", "r", stdin);
    	freopen("e.out", "w", stdout);
    	scanf("%d%d%d", &N, &M, &K);
    	f[0][0][0] = 1;
    	int len = (N + M - 1) / (K - 1);
    	int ans = 0;
    	for (int i = 1; i <= len; ++i) {
    		s[0] = Ad(f[i - 1][0][0], f[i - 1][0][1]);
    		for (int j = 1; j <= M; ++j)
    			s[j] = Ad(s[j - 1], Ad(f[i - 1][j][0], f[i - 1][j][1]));
    		for (int j = 0; j <= M; ++j) {
    			f[i][j][0] = Ad(f[i - 1][j][0], f[i - 1][j][1]);
    			f[i][j][1] = Sum(j - min(K - 1, j), j - 1);
    		}
    		for (int j = 0; j <= M; ++j) {
    			int tmp = i * (K - 1) + 1 - j;
    			if (j % (K - 1) == M % (K - 1) && tmp <= N && tmp % (K - 1) == N % (K - 1))
    				ans = Ad(ans, f[i][j][1]);
    		}
    	}
    	printf("%d
    ", ans);
    	fclose(stdin);
    	fclose(stdout);
    	return 0;
    }
    
  • 相关阅读:
    【笔记】Hierarchical Attention Networks for Document Classification
    Chart Parser 中 Earley's 算法的应用
    使用 JFlex 生成词法分析器的安装配置及简单示例
    UNIX 系统下退出 git commit 编辑器
    SQL语法
    MySQL 和 Javaweb 的报错合集
    最短路径(SP)问题相关算法与模板
    dfs | Security Badges
    redis哨兵机制图谱
    docker笔记
  • 原文地址:https://www.cnblogs.com/YouthRhythms/p/13775680.html
Copyright © 2020-2023  润新知