2025牛客寒假算法基础集训营2
A. 一起奏响历史之音!
题意:判断7个数里有没有出现4或7.
点击查看代码
void solve() {
int a[7];
for (int i = 0; i < 7; ++ i) {
std::cin >> a[i];
}
for (int i = 0; i < 7; ++ i) {
if (a[i] == 4 || a[i] == 7) {
std::cout << "NO\n";
return;
}
}
std::cout << "YES\n";
}
B. 能去你家蹭口饭吃吗
题意:给你一个数组,问最大的满足小于\(a\)数组一半数以上的数字是多少。
排序后取中位数减一即可。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
std::sort(a.begin(), a.end());
std::cout << a[n / 2] - 1 << "\n";
}
C. 字符串外串
题意:字符串的可爱度定义为最大的\(k\)满足有一个长度为\(k\)的字串与一个不完全连续的子序列相等。要求构造一个长度为\(n\)可爱度为\(m\)的字符串。
\(n\)小于\(m+1\)显然无解,大于\(m+26\)也无解,因为我们至少需要\(m+1\)长度来构造,然后因为小写字母只有\(26\)个,那么每\(27\)个字符必然有一个重复的,那么会导致有一个长度大于\(m\)的串也能找到一个相等的子序列。
关于构造方式,可以"abcd..xyz"\(26\)个字符串一直填,直到剩余长度小于\(26\),然后前面加上"abcd..xyz"这个串的前缀即可。
点击查看代码
void solve() {
int n, m;
std::cin >> n >> m;
if (n < m + 1 || n > m + 1 + 25) {
std::cout << "NO\n";
return;
}
std::string t;
for (int i = 0; i < 26; ++ i) {
t += (char)('a' + i);
}
std::cout << "YES\n";
std::string s;
while (s.size() + 26 <= m) {
s += t;
}
s = s + t.substr(0, m - (int)s.size());
s = t.substr(0, n - (int)s.size()) + s;
std::cout << s << "\n";
}
D. 字符串里串
题意:字符串的可爱度定义为最大的\(k\)满足有一个长度为\(k\)的字串与一个不完全连续的子序列相等。给你一个字符串,求它的可爱度。
对于任意长度为\(K\)的一个字串,如果它的第一个字符在它的前面也有出现,或者它的最后一个字符在它后面也有出现,这个字符串就是\(k\)可爱的。可以直接找第二次出现位置最前的字符和倒数第二次出现位置最后的字符,他们的区间长度就是答案,但赛时没想到这一点,写了个二分。因为如果有长度为\(k\)的字串满足,那么\(k-1\)的也有,可以二分,每次枚举所有\(mid\)长度的字串看它的第一个字符在它的前面有没有出现,或者它的最后一个字符在它后面有没有出现,存下来所有字符出现的位置,check可以\(O(n)\)。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::string s;
std::cin >> s;
std::vector<std::vector<int> > pos(26);
for (int i = 0; i < n; ++ i) {
pos[s[i] - 'a'].push_back(i);
}
auto check = [&](int len) -> bool {
for (int i = 0; i + len - 1 < n; ++ i) {
if (i != pos[s[i] - 'a'][0] || i + len - 1 != pos[s[i + len - 1] - 'a'].back()) {
return true;
}
}
return false;
};
int l = 1, r = n - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
if (!check(l)) {
std::cout << 0 << "\n";
} else {
std::cout << l << "\n";
}
}
E. 一起走很长的路
题意:给你一个数字,\(q\)次询问,每次问一个区间\([l, r]\),对于\(i \in [l + 1, r]\)的每个\(i\),是不是都有\(a_i > \sum_{j=l}^{i-1} a_j\),你可以每次个一个数加一或者减一,问最后操作多少次才能满足条件。
如果我们给前面的加一,它会使得后面的数更容易满足条件,减一则可能导致本来满足结果小于后面的数了。那么我们应该只进行加一操作。设\(sum_i = \sum_{j=1}^{i} a_j\),那么意味着对于\(i \in [l + 1, r]\),都要满足\(a_i - (sum_{i-1} - sum_{l-1}) <= 0\),那么我们维护\(a_i - sum_{i-1}\)的最大值,就知道最少需要加多少。
点击查看代码
#define ls (u << 1)
#define rs (u << 1 | 1)
#define umid (tr[u].l + tr[u].r >> 1)
struct Node {
int l, r;
i64 max;
};
const int N = 2e5 + 5;
i64 a[N], sum[N];
struct SegmentTree {
std::vector<Node> tr;
SegmentTree(int _n) {
tr.assign(_n << 2, {});
build(1, _n);
}
void pushup(int u) {
tr[u].max = std::max(tr[ls].max, tr[rs].max);
}
void build(int l, int r, int u = 1) {
tr[u] = {l, r};
if (l == r) {
tr[u].max = a[l] - sum[l - 1];
return;
}
int mid = l + r >> 1;
build(l, mid, ls); build(mid + 1, r, rs);
pushup(u);
}
i64 query(int l, int r, int u = 1) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].max;
}
int mid = umid;
if (r <= mid) {
return query(l, r, ls);
} else if (l > mid) {
return query(l, r, rs);
}
return std::max(query(l, r, ls), query(l, r, rs));
}
};
void solve() {
int n, q;
std::cin >> n >> q;
for (int i = 1; i <= n; ++ i) {
std::cin >> a[i];
sum[i] = sum[i - 1] + a[i];
}
SegmentTree tr(n);
while (q -- ) {
int l, r;
std::cin >> l >> r;
if (l == r) {
std::cout << 0 << "\n";
} else {
i64 max = tr.query(l + 1, r);
std::cout << std::max(0ll, max + sum[l - 1]) << "\n";
}
}
}
F. 起找神秘的数!
题意:求\([l, r]\)内有多少\(x + y = (x \ and \ y) + (x \ or \ y) + (x \ xor \ y)\)。
我们发现,与运算是取\(x, y\)都有的1,异或运算是取\(x, y\)这一位恰好一个1的1,而或运算是只要有1就算上,那么\((x \ and \ y) + (x \ xor \ y) = (x \ or \ y)\)。那么我们要求\(x + y = 2(x \ or \ y)\),因为\(x + y >= 2(x \ or \ y)\),发现只有\(x = y\)的时候才能满足。
点击查看代码
void solve() {
i64 l, r;
std::cin >> l >> r;
std::cout << r - l + 1 << "\n";
}
G. 一起铸最好的剑!
题意:求\(|n - m^x|\)值最小的\(x\)。
特判\(m=1\)的情况,其他情况模拟即可,因为\(n\)最多\(1e18\),\(x\)最多取到60。
点击查看代码
void solve() {
i64 n, m;
std::cin >> n >> m;
i64 ans = 1, d = std::abs(n - m);
if (m == 1) {
std::cout << 1 << "\n";
return;
}
using i128 = __int128;
auto abs = [&](i128 x) -> i128 {
return x >= 0 ? x : -x;
};
i128 x = m * m;
for (int i = 2; x <= n + d; x *= m, ++ i) {
if (abs(n - x) < d) {
d = abs(n - x);
ans = i;
}
}
std::cout << ans << "\n";
}
H. 一起画很大的圆!
题意:在一个矩形的边上找三个点,使得可以让边经过这三个点的圆半径最大。
假设三个点是\(A, B, C\),那么我们要让\(AB, BC\)的中垂线角度最大,那么可以选长边最边上的两个点,和短边上距离长边最近的点,这样的角度是最大的。
点击查看代码
void solve() {
int a, b, c, d;
std::cin >> a >> b >> c >> d;
if (b - a >= d - c) {
std::cout << a << " " << c << "\n";
std::cout << a + 1 << " " << c << "\n";
std::cout << b << " " << c + 1 << "\n";
} else {
std::cout << a << " " << c << "\n";
std::cout << a << " " << c + 1 << "\n";
std::cout << a + 1 << " " << d << "\n";
}
}
I. 一起看很美的日落!
题意:给你一棵树,每个节点有点权。一个连通块的值定义为连通块里的任意两个点异或和的和。求树上所有连通块值的和。
对于位运算,我们尽可能考虑按位求和。
这题明显是树形dp,那么我们考虑如何计算贡献。记\(dp_u\)为以\(u\)为根的子树中所有连通块的和。\(cnt_u\)记为以\(u\)为根的子树中连通块的数量。\(f_{u_{0/1}}\)为以\(u\)为根的子树中所有连通块的\(0\)/\(1\)的个数。那么当我们计算第\(i\)位时,\(u\)枚举到\(v\)这棵子树时,可以得\(dp_u = dp_u + dp_u \times cnt_v + dp_v \times dp_u + 2^i \times (f_{u_0} \times f_{v_1} + f_{u_1} \times f_{v_0})\)。
其中\(dp_u \times cnt_v\)为\(u\)已经计算过的子树贡献乘上\(v\)这棵子树有多少个连通块,因为每多一个连通块,之前任何一个连通块里的贡献都要再加一次。\(dp_v \times dp_u\)同理。\(2^i \times (f_{u_0} \times f_{v_1} + f_{u_1} \times f_{v_0})\)表示已经计算过的子树和\(v\)这棵子树两者之间增加的贡献,那就是看有多少个\(1\)去和\(0\)异或和有多少\(0\)去和\(1\)异或,因为是在计算第\(i\)位,所以要乘上\(2^i\)。
\(f_u\)的计算也是类似的,\(f_{u_0} = f_{u_0} + f_{u_0} \times cnt_v + f_{v_0} \times cnt_u\),\(f_{u_1} = f_{u_1} + f_{u_1} \times cnt_v + f_{v_1} \times cnt_u\)。
\(cnt\)的贡献则简单算。\(cnt_u = cnt_u + cnt_u \times cnt_v\)。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
std::vector<std::vector<int> > adj(n);
for (int i = 1; i < n; ++ i) {
int u, v;
std::cin >> u >> v;
-- u, -- v;
adj[u].push_back(v);
adj[v].push_back(u);
}
std::vector dp(n, std::array<Z, 30>{});
std::vector f(n, std::array<std::array<Z, 2>, 30>{});
std::vector<Z> cnt(n);
Z ans = 0;
auto dfs = [&](auto self, int u, int fa) -> void {
cnt[u] = 1;
for (int i = 0; i < 30; ++ i) {
f[u][i][~a[u] >> i & 1] = 0;
f[u][i][a[u] >> i & 1] = 1;
}
for (auto & v : adj[u]) {
if (v == fa) {
continue;
}
self(self, v, u);
for (int i = 0; i < 30; ++ i) {
dp[u][i] += dp[u][i] * cnt[v] + dp[v][i] * cnt[u] +
(f[u][i][0] * f[v][i][1] + f[u][i][1] * f[v][i][0]) * (Z)(1 << i);
f[u][i][0] += f[u][i][0] * cnt[v] + f[v][i][0] * cnt[u];
f[u][i][1] += f[u][i][1] * cnt[v] + f[v][i][1] * cnt[u];
}
cnt[u] += cnt[u] * cnt[v];
}
for (int i = 0; i < 30; ++ i) {
ans += dp[u][i];
}
};
dfs(dfs, 0, -1);
std::cout << ans * 2 << "\n";
}
J. 数据时间?
题意:统计某一天的三个时间段有多少人。
用\(string\)比较每个时间段确定是哪个时间段即可,答案用\(set\)存可以去重。
点击查看代码
void solve() {
int n;
std::string y, m;
std::cin >> n >> y >> m;
if (m.size() == 1) {
m = "0" + m;
}
std::string l[5] = {"07:00:00", "11:00:00", "22:00:00", "00:00:00", "18:00:00"};
std::string r[5] = {"09:00:00", "13:00:00", "23:59:59", "01:00:00", "20:00:00"};
std::array<std::set<std::string>, 3> s{};
for (int i = 0; i < n; ++ i) {
std::string id, a, b;
std::cin >> id >> a >> b;
if (a.substr(0, 4) != y || a.substr(5, 2) != m) {
continue;
}
if ((b >= l[2] && b <= r[2]) || (b >= l[3] && b <= r[3])) {
s[2].insert(id);
} else if (b >= l[1] && b <= r[1]) {
s[1].insert(id);
} else if ((b >= l[0] && b <= r[0]) || (b >= l[4] && b <= r[4])) {
s[0].insert(id);
}
}
for (int i = 0; i < 3; ++ i) {
std::cout << s[i].size() << " \n"[i == 2];
}
}
K. 可以分开吗?
题意:求每个\(1\)的连通块周围0的数量最少的连通块。
\(dfs\)搜索即可,可以用\(set\)存\(0\)的位置。
点击查看代码
void solve() {
int n, m;
std::cin >> n >> m;
std::vector<std::string> s(n);
for (int i = 0; i < n; ++ i) {
std::cin >> s[i];
}
const int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
std::set<std::pair<int, int> > set;
std::vector vis(n, std::vector<int>(m));
auto dfs = [&](auto self, int x, int y) -> void {
if (s[x][y] == '0') {
set.insert({x, y});
return;
}
vis[x][y] = 1;
for (int i = 0; i < 4; ++ i) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m || vis[nx][ny]) {
continue;
}
self(self, nx, ny);
}
};
int ans = n * m;
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
if (!vis[i][j] && s[i][j] == '1') {
set.clear();
dfs(dfs, i, j);
ans = std::min(ans, (int)set.size());
}
}
}
std::cout << ans << "\n";
}
M. 那是我们的影子
题意:有一个\(3 \times n\)的数独,要求每一个\(3 \times 3\)的子矩阵中\(1\)到\(9\)都恰好出现一次。现在有些位置已经填好数了,问你有多少种可能的答案。
首先判断无解的情况,发现第\(i\)行和第\(i+3\)行公用两列,那么它们两行填的数必须一样。然后一列里同一个数只能有一个,再就是判断每个\(3\times3\)的格子是不是合法的。
然后考虑怎么填,根据之前的发现,第\(i\)行\(i+3\)行要填一样的数,那么我们只要枚举前三列怎么填就可以知道后面怎么填了。那么枚举全排列即可,因为\(9!\)使比较大的计算量,所以判断排列是否合法要用尽量快的方法。比如可以把每个\(i%3\)列必须填的数字按状压的方式用一个数字表示,那么可以用位运算快速判断是否填了必须填的数。
知道有多少排列合法后,后面每列的填法就是看有多少个问号,乘起来就行了。
点击查看代码
const int mod = 1e9 + 7;
void solve() {
int n;
std::cin >> n;
std::vector<std::string> s(3);
for (int i = 0; i < 3; ++ i) {
std::cin >> s[i];
}
std::vector<std::set<int> > nums(3);
for (int j = 0; j < n; ++ j) {
if (j + 2 < n) {
std::vector<int> cnt(10);
for (int i = 0; i < 3; ++ i) {
for (int k = j; k < j + 3; ++ k) {
if (s[i][k] != '?' && ++ cnt[s[i][k] - '0'] > 1) {
std::cout << 0 << "\n";
return;
}
}
}
}
std::vector<int> cnt(10);
for (int i = 0; i < 3; ++ i) {
if (s[i][j] != '?') {
if (++ cnt[s[i][j] - '0'] > 1) {
std::cout << 0 << "\n";
return;
}
nums[j % 3].insert(s[i][j] - '0');
}
}
}
if (nums[0].size() > 3 || nums[1].size() > 3 || nums[2].size() > 3) {
std::cout << 0 << "\n";
return;
}
std::vector<int> st(3);
for (int i = 0; i < 3; ++ i) {
for (auto & x : nums[i]) {
st[i] |= 1 << x;
}
}
std::vector<int> p(9);
std::iota(p.begin(), p.end(), 1);
i64 ans = 0;
do {
int flag = 1;
for (int i = 0; i < 3 && flag; ++ i) {
for (int j = 0; j < 3; ++ j) {
if (s[i][j] != '?' && s[i][j] - '0' != p[i * 3 + j]) {
flag = 0;
break;
}
}
}
if (!flag) {
continue;
}
for (int i = 0; i < 3; ++ i) {
int x = (1 << p[i]) + (1 << p[i + 3]) + (1 << p[i + 6]);
if ((x & st[i]) != st[i]) {
flag = 0;
break;
}
}
ans += flag;
} while (next_permutation(p.begin(), p.end()));
i64 v[4] = {1, 1, 2, 6};
for (int i = 3; i < n; ++ i) {
int cnt = (s[0][i] == '?') + (s[1][i] == '?') + (s[2][i] == '?');
ans = (ans * v[cnt]) % mod;
}
std::cout << ans << "\n";
}