2019年清华大学计算机系夏令营机试题目题解

题目均来自于网上公开资源或机试参与者的分享,侵删。仅供学习交流,禁止一切形式的转载。

首发于知乎专栏「编程机试指南」
作者@知乎:Xanthus Deng

第一题

题意
现定义一种字符串加密方式:将字符串每个字符的值在字母表上向右移一位
右移的规则是’a’ -> ‘b’, ‘b’ -> ‘c’, …, ‘y’ -> ‘z’, ‘z’->‘a’,例如"afju"加密后为"bgkv"。
现给定正整数 n ( n ≤ 1 e 18 ) n(n\leq1e18) n(n1e18) 和一个长度不超过 1 e 6 1e6 1e6的只含小写字母的字符串 s s s ,已知 s s s 被加密了 n n n次,输出加密前的字符串。

题解
首先题目给出了加密的定义,但实际要求做的是解密,故可以for循环遍历字符串每一个字符,将其左移 n n n次即可。

但题目中 n n n的值高达 1 e 18 1e18 1e18 ,直接左移显然会超时。

经过观察,容易发现加密和解密操作具有周期性,即将一个字符左移或右移26次值将不变。

所以先将 n n n 对 26 取模,再进行左移操作即可。
注意’a’左移一次后将变成’z’的特殊情况和 n 需要用长整数存储。

代码

#include 
#include 
using namespace std;
int main() {
	long long n;
	string s;
	cin >> n >> s;
	
	n %= 26;
	int len = s.length();
	for (int i = 0; i < len; i++) {
		s[i] = (s[i] - 'a' - n + 26) % 26 + 'a';
	}
	cout << s << endl;
	return 0;
}

第二题

题意
给定一个节点数为 n ( n ≤ 1 e 6 ) n(n\leq1e6) n(n1e6) 的树,节点编号为 1 , 2 , 3... n 1, 2, 3...n 1,2,3...n ,其中节点 1 1 1 为根节点。
每个节点上保存了一个数字,数字的值在int范围内 ( − 2147483648 ∼ 2147483647 ) (-2147483648 \sim 2147483647) (21474836482147483647)
求对于树上的每一个节点,从根节点到该节点的路径上,有多少种不同的数字,按编号依次输出。

题解
考察算法dfs序

对于这种树上或者图上的题目,一个小技巧是可以先考虑线性情况下,我们应该怎么做:

给定一个包含 n 个数的数组,依次输出对于第 i ( 1 ≤ i ≤ n ) i(1 \leq i \leq n) i(1in) 个数,从第 1 1 1 个数到第 i i i 个数,出现了多少种不同的数?
显然我们可以开一个计数数组 c n t cnt cnt c n t [ i ] cnt[i] cnt[i] 表示值为 i i i 的数出现的次数,然后遍历整个数组,如果当前数为 x x x ,则 c n t [ x ] + = 1 cnt[x] += 1 cnt[x]+=1 ,如果加 1 1 1 c n t [ x ] cnt[x] cnt[x] 值为 0 0 0,则答案 + 1 +1 +1

另外根据题意,值在int范围内,显然我们的 cnt 数组不可能开这么大的内存,故需要离散化

代码:

#include 
#include 
#include 
#include 
using namespace std;

const int A = 1e6 + 10;
int a[A], cnt[A];
vector<int> vec;

int main() {
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		vec.push_back(a[i]);	
	}
	
	// 离散化 
	sort(vec.begin(), vec.end());
	vec.erase(unique(vec.begin(), vec.end()), vec.end());
	for (int i = 1; i <= n; i++) {
		a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin() + 1;
	}
	
	memset(cnt, 0, sizeof(cnt));
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		if (cnt[a[i]] == 0) ans++;
		cnt[a[i]]++;
                cout << ans << endl;
	}
	return 0;
}

但对于树的情况,我们应该怎么处理呢?
考虑深度优先搜索(dfs)遍历整棵树,假设对于下面这种情况(手残党的图QwQ):

2019年清华大学计算机系夏令营机试题目题解_第1张图片

黑色的数字代表遍历顺序。
橙色的线代表向下搜索,蓝色的线代表向上回溯。
当前是从根节点遍历到6号节点的搜索路径情况。
如果蓝色经过的路径会与橙色路径相抵消的话,这张图会变成什么样呢?如下图所示:

2019年清华大学计算机系夏令营机试题目题解_第2张图片

此时恰好是从根节点到6号节点的路径,可以等价于一维的问题处理。
现在唯一的问题时,如何让橙色和蓝色的路径抵消呢?即如何让向下搜索和向上回溯对 cnt 数组的影响抵消掉呢?
很明显,向下搜索的时候,需要不断更新 cnt 数组,每次进行加1操作。
向上回溯的时候,则需要不断更新 cnt 数组,不过是进行减1操作。
此题得解。

代码

#include 
#include 
#include 
#include 
using namespace std;

const int A = 1e6 + 10;
class Tree {
public:
	int v, next;
}tree[A<<1];   //建立双向边,故边数为点数的两倍 
int a[A], cnt[A], head[A], tot, res, ans[A];
vector<int> vec;

void add (int u, int v) {
	tree[tot].v = v;
	tree[tot].next = head[u];
	head[u] = tot++;
} 

void init() {
	memset(cnt, 0, sizeof(cnt));
	memset(head, -1, sizeof(head));
	tot = 0;
}

void dfs(int u, int fa) {
	if (cnt[a[u]] == 0) res++;
	cnt[a[u]]++;
	ans[u] = res;
	for (int i = head[u]; i != -1; i = tree[i].next) {
		int v = tree[i].v;
		if (v == fa) continue;
		dfs(v, u);
	}
	cnt[a[u]]--;
	if (cnt[a[u]] == 0) res--;
}

int main() {
	init();
	
	int n;
	cin >> n;   //输入树的节点数 
	for (int i = 1; i <= n; i++) {
		cin >> a[i];  //输入树每个节点的值 
		vec.push_back(a[i]);	
	}
	for (int i = 1; i < n; i++) {
		int u, v;
		cin >> u >> v;  //输入n-1条边,(u, v)表示节点u与节点v之间有一条边 
		add(u, v);
		add(v, u);
	}
	
	// 离散化 
	sort(vec.begin(), vec.end());
	vec.erase(unique(vec.begin(), vec.end()), vec.end());
	for (int i = 1; i <= n; i++) {
		a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin() + 1;
	}
	
	res = 0;
	dfs(1, 1);
	for (int i = 1; i <= n; i++) {
		cout << ans[i] << endl;
	}
	return 0;
}

第三题

题意
给定一个 n ( n ≤ 3 e 5 ) n(n\leq 3e5) n(n3e5)个点, m ( m ≤ 1 e 6 ) m(m\leq1e6) m(m1e6)条边的有向图,每个节点有一个数,现在要求找到一条路径,路径节点中的最大值减去最小值的差最大,即路径上节点值的极差最大,输出最大的差。

题解
此题解法很多。

首先可以考虑求出强连通分量,然后对其进行缩点,然后每个节点维护以该节点为终点的路径中的最大值和最小值,然后对DAG直接搜索即可。

也可以考虑图上的动态规划。

下面详细讲一个理解更简单的方法

分享又一个小技巧:如果遇到没有思路的算法题,可以思考一下暴力能怎么做,然后思考效率差在哪里,应该怎么去优化。

此题显然有一个暴力的做法,即遍历每一个点,然后以每一个点作为起点,搜索所有可能的路径,然后求其中最大的极差。

显然,效率差的原因在于,当遍历每一个点作为起点进行搜索时,可能存在一些点被重复搜索多次

最坏情况下,这个图是一个环,那么每个点在另外的点作为起点时,都会被搜索到,时间复杂度 O ( n 2 ) O(n^2 ) O(n2)

那么怎么避免点被重复搜索到呢。

一个很显然的思路是,可以先将点按数值的大小进行排序,然后从数值最小的点开始遍历路径的起点。

如果当前起点为 i i i ,第 i i i 个点的数值为 v [ i ] v[i] v[i]
则当一个点 x x x 被搜索到时,此时的极差为 v [ x ] − v [ i ] v[x] - v[i] v[x]v[i]

对于之后新的起点 j j j ,因为排序的原因,有 v [ j ] > v [ i ] v[j] > v[i] v[j]>v[i]
如果从 j 开始也能搜索到节点 x x x ,显然有 v [ x ] − v [ j ] < v [ x ] − v [ i ] v[x] - v[j] < v[x] - v[i] v[x]v[j]<v[x]v[i],故每个点只需要被搜索一次,一旦搜索到了就可以进行标记,后面的节点作为起点再次搜索到时,就可以直接退出,不需要继续向下搜索。

因为每个点只会被搜索到一次,时间复杂度为 O ( n ) O(n) O(n)

代码:

#include 
#include 
#include 
#include 
using namespace std;

const int A = 1e6 + 10;
const int B = 3e5 + 10;
class Gra{
public:
	int v, next;
}g[A<<1];   

class Node{
public:
	int id, val;
	bool operator<(const Node& rhs) const{
        return val < rhs.val;
    }
}node[B]; 

int head[B], tot, ans, mn, mx, a[B];
bool vis[B];

void add (int u, int v) {
	g[tot].v = v;
	g[tot].next = head[u];
	head[u] = tot++;
} 

void init() {
	memset(head, -1, sizeof(head));
	memset(vis, 0, sizeof(vis));
	tot = 0;
}

void dfs(int u) {
	if (vis[u]) return;
	vis[u] = 1;
	mx = max(mx, a[u]);
	ans = max(ans, mx - mn);
	
	for (int i = head[u]; i != -1; i = g[i].next) {
		int v = g[i].v;
		dfs(v);
	}
}

int main() {
	init();
	
	int n, m;
	cin >> n >> m;   //输入图点数和边数
	for (int i = 1; i <= n; i++) {
		node[i].id = i;
		cin >> a[i];
		node[i].val = a[i];
	} 
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;  //输入m条边的信息,(u, v)表示有一条u到v的边 
		add(u, v);
	}
	sort(node + 1, node + 1 + n);
	
	ans = 0;
	for (int i = 1; i <= n; i++) {
		mn = mx = node[i].val;
		dfs(node[i].id);
	}
	cout << ans << endl;
	return 0;
}

你可能感兴趣的:(2019年清华大学计算机系夏令营机试题目题解)