CF1464C Poman Numbers
Codeforces, Codeforces Round #692 (Div. 1, based on Technocup 2021 Elimination Round 3), CF#692, CF1464C Poman Numbers
题目大意
对一个由小写字母组成的非空字符串 (S),定义它可以对应的数值 (f(S)):
- 如果 (|S| > 1),则可以任选一个 (1leq m < |S|),并令 (f(S) = -f(S[1,m]) + f(S[m + 1, |S|]))。
- 否则设 (S) 里唯一的字符为 (c),则 (f(S) = 2^{ ext{pos}(c)})。其中 ( ext{pos}(c)) 表示字符 (c) 在字母表中的位置,( ext{pos}( exttt{a}) = 0, ext{pos}( exttt{z}) = 25)。
每一步里的 (m) 可以独立选择。
给你一个长度为 (n) 的字符串 (S),和一个整数 (T)。请你判断 (f(S)) 是否可能等于 (T)。
数据范围:(1leq nleq 10^5),(-10^{15}leq Tleq 10^{15})。
本题题解
考虑 (f(S[1,n])) 递归的过程,相当于建出一棵二叉树,满足这棵二叉树恰有 (n) 个叶子,且每个非叶子节点恰有两个儿子。从左到右,每个叶子依次对应了 (S[1,n]) 里的每个字符。
那么我们可以将 (f(S[1,n])) 的值拆成 (n) 个叶子的贡献。对每个叶子,考虑从它到根的路径,设其中有 ( ext{leftStep}) 步是向左走的,那么它最终的贡献系数就是 ((-1)^{ ext{leftStep}})。
引理一:
(S_n) 的贡献系数必须为 (1),(S_{n - 1}) 的贡献系数必须为 (-1)。除 (S_n) 和 (S_{n - 1}) 外,其他每个叶子的贡献系数无论怎么选择,都能构造出对应的二叉树。
证明:
设每个节点的贡献系数分别为:(c_1,c_2,dots,c_n)。((forall i: c_iin{-1,1}))。
(S_n) 在二叉树上只能是一直向右(因为每个非叶子节点必须有两个儿子),所以贡献系数 (c_n) 一定是 (1)。
又因为 (S_{n - 1}) 是 (S_n) 的兄弟,所以它们只有最下面一步不同((n-1) 向左,(n) 向右),因此 (c_{n - 1} = -1)。
对于其他节点,考虑递归地解决。初始时 (C = {c_1,c_2,dots,c_n}) ((ngeq 2))。
- 若当前 (c_1 = -1),则递归 (C_L = {c_1}, C_R = {c_2, c_3, dots, c_{|C|}})。
- 若当前 (c_1 = 1),则找到它后面第一个 (c_p = -1) 的位置 (p)(显然 (p < |C|))。递归 (C_L = {-c_1,-c_2,dots , -c_p}, C_R = {c_{p + 1}, dots ,c_{|c|}})。发现两个集合都仍然满足 (c_{|C|} = 1, c_{|C| - 1} = -1)。
边界是 (|C| = 1) 时,就已经自动完成了构造。
于是问题转化为,要确定一个 (c) 序列,满足 (forall i: c_i in{-1,1}),且 (c_n = 1, c_{n - 1} = -1),使得 (sum_{i = 1}^{n}c_i 2^{ ext{pos}(S_i)} = T)。
不妨先假设 (forall 1leq ileq n - 2),(c_i = -1)。求出此时的和 ( ext{sum} = -sum_{i = 1}^{n-1}2^{ ext{pos}(S_i)} + 2^{ ext{pos}(S_n)})。这是能得到的最小值。若 (T < ext{sum}) 则一定无解。否则令 (T'= T - ext{sum})。
然后问题进一步转化为:给定 (n - 2) 个形如 (2^k) 的数 (a_1,dots,a_{n - 2}),问是否能从中选出一个子集,使得和为 (T')。其中 (1leq kleq 26),注意不是 (0) 到 (25),因为从 (-1) 变成 (1) 相当于增大两倍。
引理二:
对于任意整数 (Kgeq 1),如果一个可重集 (A = {a_1,a_2,dots,a_m}) 满足所有 (a_i) 形如 (2^k) ((0leq k < K),(k) 是整数),且所有数之和大于 (2^K)。那么一定存在一个子集 (Bsubset A),满足 (B) 里所有数之和等于 (2^K)。
证明:
当 (K = 1) 时显然正确。
当 (K > 1) 时,用归纳法。假设已经对 (K - 1) 成立。考虑 (K),先把 (A) 里所有 (2^{K - 1}) 挑出来,如果有至少 (2) 个 (2^{K - 1}),那么已经达到要求。否则用剩下的数去拼 (2^{K - 1}),根据归纳假设,一定能恰好拼出所需的数量。
回到本题,我们从大到小考虑 (a_1,dots, a_{n - 2}) 里所有数。如果当前数 (a_i leq T'),就选择 (a_i),并令 (T') 减去 (a_i)。最终如果 (T' = 0),答案就是 ( exttt{Yes}),否则是 ( exttt{No})。根据引理二可以说明这个贪心的正确性。
时间复杂度 (mathcal{O}(n + Sigma)),(Sigma = 26)。
参考代码
// problem: CF1464C
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 1e5;
int n;
char s[MAXN + 5];
ll goal;
int cnt[26];
int main() {
cin >> n >> goal;
cin >> (s + 1);
goal -= (1LL << (s[n] - 'a')); // s[n] 一定是正号
goal += (1LL << (s[n - 1] - 'a')); // s[n - 1] 一定是负号
ll low = 0;
for (int i = 1; i <= n - 2; ++i) {
int c = (s[i] - 'a');
cnt[c]++;
low -= (1LL << c);
}
if (goal < low) {
cout << "No" << endl;
return 0;
}
goal = goal - low;
for (int i = 25; i >= 0; --i) {
ll v = (1LL << (i + 1));
ll x = min(goal / v, (ll)cnt[i]);
goal -= x * v;
}
if (goal) {
cout << "No" << endl;
} else {
cout << "Yes" << endl;
}
return 0;
}