A

算法:模拟【1】

顺序遍历字符串 3 遍,第一遍输出小写字母,第二遍输出数字,第三遍输出大写字母。

B

算法:数学【1】/ 二分【4】()

我们考虑一个大致的位置 ans = b * c / d,然后再来偏移一下,具体二分过不了不知道是为什么?

C

算法:模拟【1】

考虑到 n 和 c 很小,所以直接枚举 a,则 b = c - a,使用 STL 中的 to_string() 函数化为计算式,然后和 n 比较即可。

D

算法:构造【4】

考虑构造,我们发现当 n = k 时,我们全部都必须是一个数字,当 k = 1 时,我们应当每个数字都不同。

然后我们来考虑 的时候的情况。我们发现当 k = n-1 的时候,只要交替循环两个数字即可。这个思路很有拓展的前景,我们发现因为我们钦定最长不同区间的大小为 (两个数字交替),我们可以通过往区间的后缀全部覆盖为其中一个数字即可实现使得区间的这一段后缀一定不会产生贡献,相当于我们删除掉了这个数列的一个后缀。这时候按照上述思路,我们只要删掉一部分后缀,使得 k = n - 1 即可完成构造。

可以证明当 n > k 的时候,不存在任何构造方案。

E

算法:动态规划【5】

考虑 表示前 位,满足数位和为 j 的话,最后最大能够得到多大的值。

数位的话,转移方程都很显然,我们来看看实现吧:

#include <bits/stdc++.h>

#define L(i, a, b) for(int i = (a); i <= (b); i++)
#define R(i, a, b) for(int i = (a); i >= (b); i--)
#define ll long long

using namespace std;

const int N = 20 + 10;

int F[N][N * 10]; ll l1, l2, r1, r2;

int l[N], r[N]; int len = 0;

int dfs(int x, bool lx, bool ls, int sum) {
	if(x == 0) return sum;
	if(!lx && !ls && F[x][sum] != -1) return F[x][sum];
	int ret = 0;
	int xj = (lx ? l[x] : 0);
	int sj = (ls ? r[x] : 9);
	for(int i = xj; i <= sj; i++)
	  ret = max(ret, dfs(x-1, (lx && i == xj), (ls && i == sj), sum + i));
	if(!lx && !ls) F[x][sum] = ret;
//	cout << ret << '\n';
	return ret;
}

void solve() {
	cin >> l1 >> r1 >> l2 >> r2;
	l1 += l2; r1 += r2;
	len = 0;
	memset(F, -1, sizeof(F));
	while(r1) {
		r[++len] = r1 % 10;
		r1 /= 10;
	}
	L(i, 1, len) l[i] = l1 % 10, l1 /= 10;
	cout << dfs(len, 1, 1, 0) << '\n';
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);  cout.tie(0);
	int T; cin >> T;
	while(T--)
	  solve();
	return 0;
}



F

考虑树上启发式合并和树上前缀和。

我们将所有询问离线下来,附在 u 节点上面。

最后的答案为 =

考虑所有从 u 节点出发的询问。

我们在启发式合并的时候要维护当前子树内每个深度的 sum 的最大值。

这样询问下来的时间复杂度是可以接受的。