HD 2025 春季联赛 3
1001
数学 Lucas定理
解题思路
一看题就知道我们要求当 \(b_i \in [1, L_i]\) 内,有多少 \(b_i\) 使得 \((^{a_i}_{b_i})\) 是奇数。
于是我们看 \(Lucas\) 定理:
对于素数 \(p\),有:
\[(^n_k) \equiv (^{\lfloor{n \over p}\rfloor}_{\lfloor{k \over p}\rfloor})(^{n \% p}_{k \% p}) (mod\ p) \]其中当 \(n < k\) 时, \((^n_k) = 0\)。
当我们将 2 代入 \(p\),我们还可以这样看 \(lucas\) 定理:
其中 \(x >> 1\) 就是 \(\lfloor {x \over 2} \rfloor\)。
当 \(p\) 等于 2 时,我们可以发现 \((^0_1) = 0\)。这就意味着若想 \((^n_k)\) 是奇数,则不能出现 \(n\) 是偶数时且 \(k\) 是奇数这种情况。推广到每一位,就是当 \(n\) 的某一位是 0 时, \(k\) 中对应的那一位不能是 1。
于是符合条件的 \(b_i\) 就显然了:既要满足在给定的范围之内,又要满足由 \(Lucas\) 定理推导出来的限制。
CODE
int a[N + 5];
int cnt(int L, int x) {
// 不大于 L 的最大的满足 x 的数
int cur = 0, res = 0;
for (int i = 30; i >= 0; i--) {
if (x >> i & 1) {
res <<= 1;
if ((cur | (1 << i)) <= L)
{
cur |= (1 << i);
res |= 1;
}
}
}
return res + 1;
}
void solve()
{
int n = 0;
std::cin >> n;
for (int i = 0; i < n; i++) {
std::cin >> a[i];
}
i64 ans = 1ll;
for (int i = 0, L = 0; i < n; i++) {
std::cin >> L;
(ans *= cnt(L, a[i])) %= Mod;
}
std::cout << ans << '\n';
}
1003
思维 数据结构
解题思路
先将靠初始值就满足条件的公司都放在一个队列里,对于初始值不满足条件的公司,我们记录下有多少项不满足条件,并且对于每一项不满足条件的能力,我们用一个小根堆维护(后面能力提升后就可以快速找到满足了那些条件)
CODE
i64 a[M], cnt[N]; // cnt[i] 有多少能力不满组第 i 各公司的要求
bool solve()
{
int n = 0, m = 0;
std::cin >> n >> m;
for (int i = 0; i < m; i++) {
std::cin >> a[i];
}
std::vector w(n, std::vector(m, 0));
std::vector pq(m, std::priority_queue<std::pair<int, int>>{});
std::queue<int> q;
for (int i = 0; i < n; i++) {
cnt[i] = 0;
for (int j = 0; j < m; j++) {
int c = 0;
std::cin >> c;
if (a[j] < c) {
cnt[i]++;
pq[j].push({ -c, i });
}
}
if (cnt[i] == 0) {
// 初始时就满足的直接入队
q.push(i);
}
for (int j = 0; j < m; j++) {
std::cin >> w[i][j];
}
}
while (not q.empty()) {
int cur = q.front();
q.pop();
for (int i = 0; i < m; i++) {
a[i] += w[cur][i];
while (not pq[i].empty() && -(pq[i].top().first) <= a[i]) {
auto [val, idx] = pq[i].top();
pq[i].pop();
if ((--cnt[idx]) == 0) {
// 全部满足了就入队
q.push(idx);
}
}
}
}
for (int i = 0; i < n; i++) {
if (cnt[i] != 0) {
return false;
}
}
return true;
}
1004
动态规划
解题思路
首先我们将筷子按升序排号,此时对于大部分的筷子,最优应该和相邻的配对,特殊情况是:当两个必须要拿的筷子之间隔了一双筷子,我们就要看这两个必须要拿的筷子是配成一双还是各自找其他的配对。
转移直接看代码就好了,特别要注意初始化。
CODE
std::pair<int, bool> c[N + 5];
i64 f[N + 5][2];
void solve()
{
int n = 0, m = 0;
std::cin >> n >> m;
for (int i = 1; i <= n; i++) {
std::cin >> c[i].first;
c[i].second = false;
}
for (int i = 0; i < m; i++) {
int a = 0;
std::cin >> a;
c[a].second = true;
}
std::sort(c + 1, c + 1 + n);
// 这些都是不存在的情况。
f[0][0] = f[0][1] = f[1][1] = Inf;
for (int i = 2; i <= n; i++) {
if (c[i - 1].second) { // 第 i - 1 只必须拿
f[i][0] = f[i - 1][1];
f[i][1] = f[i - 1][0] + pow(c[i].first - c[i - 1].first, 2);
}
else {
f[i][0] = std::min(f[i - 1][0], f[i - 1][1]);
f[i][1] = std::min(f[i - 1][0] + i64(pow(c[i].first - c[i - 1].first, 2)), i64(f[i - 2][0] + pow(c[i].first - c[i - 2].first, 2)));
}
}
std::cout << std::min(f[n][1], (c[n].second ? Inf : f[n][0])) << '\n';
return;
}
1005
并查集
解题思路
并查集秒了
1007
可持久化字典树
解题思路
可以发现其实 \(val = a_i \oplus x\)。于是问题就变成了在 \(i\in[l, r]\) 找一个数使得 \(a_i \oplus x\) 最大。一种不可行但直观的做法是,对于询问的每个区间,我们都去新建立一颗 01 trie,然后在 trie 上贪心地往 \(x\) 的反方向走。显然这种做法的时间复杂度太高了。我们能否通过两颗 01 trie:一颗由区间 \([1, l - 1]\) 中的数构成,另一颗由区间 \([1, r]\) 中的数构成,来找到 \(x\) 在区间 \([l, r]\) 上的答案?由于前者是后者的前缀,所以我们让两颗 trie 对应节点的大小相减就可以得知区间 \([l, r]\) 上的 01 trie 有没有该节点。用可持久化字典树就可以建立起所有 \([1, r]\) 区间上的 01 trie 然后我们就解决了问题。
CODE
constexpr int N = 2e5, Q = 2e5, Inf = 1e9;
int a[N + 5];
int rt[N + 5], cnt;
// tr[i][2] AKA siz[i]
std::array<int, 3> tr[N << 5]; // about 32N
int New() {
tr[cnt] = { 0, 0, 0 }; // init
return cnt++;
}
void insert(int cur, int old, int x) {
for (int i = 29; i >= 0; i--) {
tr[cur][2] = tr[old][2] + 1;
int b = x >> i & 1;
tr[cur][b] = New(); // likewise, persistent segment-tree, create new node to insert or modify
tr[cur][b ^ 1] = tr[old][b ^ 1];
old = tr[old][b];
cur = tr[cur][b];
}
tr[cur][2] = tr[old][2] + 1;
}
int ask(int l, int r, int x) {
int ans = 0;
for (int i = 29; ~i; i--) {
int b = x >> i & 1;
// 如果可以反着走
if (tr[tr[r][b ^ 1]][2] - tr[tr[l][b ^ 1]][2] > 0) {
b ^= 1;
ans |= (1 << i);
}
l = tr[l][b], r = tr[r][b];
}
return ans;
}
void solve()
{
cnt = 1; // started from 1
rt[0] = New();
int n = 0, q = 0;
std::cin >> n >> q;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
rt[i] = New(); // 记录每颗字典树的根节点
insert(rt[i], rt[i - 1], a[i]);
}
for (int i = 0; i < q; i++) {
int l = 0, r = 0, x = 0;
std::cin >> l >> r >> x;
std::cout << ask(rt[l - 1], rt[r], x) << '\n';
}
return;
}
1008
概率 扫描线
解题思路
要是抛开这题的期望,就单纯的求给定的区间内有多少不同的数,这就是一个比较板子的扫描线问题:模板题。实际上,本题的解法和思维难度就类似于模板题的 pro plus
假设现在我们有一段长度为 \(len\) 的区间,我们要计算在这个区间内随机选择一个的区间中包含的数的个数(包括相同的数)的期望。对于这个问题,答案的分母就是 \(len (len + 1) \over 2\),分母是 \(\sum_{i = 1}^{len}(i(len + 1 - i)) = {len(len + 1)(len + 2) \over 6}\)。
若我们要求区间中包含的不同的数的个数的期望要怎么做呢?我们就减去分子中那些重复的数字的贡献就好了。具体的,我们令 \(l(r)\) 为小于 \(r\) 的最大位置使得 \(a[l(r)] = a[r]\),于是若 \(l[r]\) 和 \(r\) 都在区间 \([L, R]\) 内,我们就认为要减去 \(a[r]\) 对分子的贡献:\((l[r] - L + 1)(R - r + 1) = (r - 1)L + (l(r) + 1)R -LR - l(r)r + l(r) - r + 1\),可以发现,\(r\) 决定的是展开式中各项的系数。这是单个位置 \(r\) 需要减去的贡献,对于所有需要减去贡献的位置的总贡献,我们只需要统计各项系数的和再将具体的 \(L\) 和 \(R\) 代入就好了。
CODE
constexpr int N = 2e5, X = 2e5;
class BIT {
private:
int n;
std::vector<i64> tr;
int lowbit(int x) {
return x & -x;
}
i64 sum(int l) {
i64 res = 0;
while (l >= 1) {
res += tr[l];
l -= lowbit(l);
}
return res;
}
public:
BIT(int _n) : n(_n) {
tr.assign(n + 1, 0ll);
}
void add(int pos, i64 val) {
while (pos <= n) {
tr[pos] += val;
pos += lowbit(pos);
}
}
i64 sum(int l, int r) {
if (l > r) {
return 0;
}
else {
return (sum(r) - sum(l - 1)) % Mod;
}
}
};
i64 qpow(i64 a, i64 p) {
i64 res = 1;
while (p) {
if (p & 1) {
(res *= a) %= Mod;
}
(a *= a) %= Mod;
p >>= 1;
}
return res;
}
i64 inv(i64 a) {
return qpow(a, Mod - 2);
}
int a[N + 5], lst[N + 5], pos[N + 5];
i64 I[N + 5], A[N + 5];
i64 ans[N + 5];
std::vector qr(N + 5, std::vector<std::array<int, 2>>{});
void solve()
{
int n = 0, q = 0;
std::cin >> n >> q;
std::fill(pos + 1, pos + 1 + n, 0);
for (int i = 1; i <= n; i++) {
qr[i].clear();
std::cin >> a[i];
lst[i] = pos[a[i]];
pos[a[i]] = i;
}
for (int i = 0; i < q; i++) {
int l = 0, r = 0;
std::cin >> l >> r;
qr[r].push_back({ l, i });
}
BIT L(n), R(n), LR(n), C(n);
for (int r = 1; r <= n; r++) {
if (lst[r] != 0) {
L.add(lst[r], r - 1);
R.add(lst[r], lst[r] + 1);
LR.add(lst[r], -1);
C.add(lst[r], -1ll * lst[r] * r + lst[r] - r + 1);
}
for (auto &[l, idx] : qr[r]) {
ans[idx] = A[r - l + 1];
(ans[idx] += Mod - (L.sum(l, r) * l + R.sum(l, r) * r + LR.sum(l, r) * l * r + C.sum(l, r)) % Mod) %= Mod;
(ans[idx] *= I[r - l + 1]) %= Mod;
}
}
for (int i = 0; i < q; i++) {
std::cout << ans[i] << '\n';
}
}
int main()
{
IOS;
int _t = 1;
std::cin >> _t;
for (int i = 1; i <= N; i++) {
I[i] = inv((1ll * (i + 1) * i / 2ll) % Mod); // 预处理分母
A[i] = (1ll * i * (i + 1) * (i + 2) / 6ll) % Mod; // 预处理分子
}
while (_t--)
{
solve();
}
system("pause");
return 0;
}
1009
并查集
解题思路
最主要的一点就是,交换部落时,我们不真的去交换部落中所有的人,而是交换两个部落之间的编号(我们称之为实际编号),这就要求我们建立起实际编号与题目所给编号(我们称之为原始编号)之间的双射关系。
CODE
int fa[N + 5], pos[N + 5], idx[N + 5], g[N + 5];
int getfa(int u) {
return u == fa[u] ? u : (fa[u] = getfa(fa[u]));
}
// u <- v;
void merge(int u, int v) {
fa[getfa(v)] = getfa(u);;
}
void solve()
{
int n = 0, q = 0;
std::cin >> n >> q;
std::iota(fa + 1, fa + 1 + n, 1);
std::iota(pos + 1, pos + 1 + n, 1);
std::iota(idx + 1, idx + 1 + n, 1);
std::iota(g + 1, g + 1 + n, 1);
while (q--) {
int op = 0;
std::cin >> op;
if (op == 1) { // 合并到 a 上
int a = 0, b = 0;
std::cin >> a >> b;
merge(pos[a], pos[b]);
}
else if (op == 2) { // 移动 a 到部落 b
int a = 0, b = 0;
std::cin >> a >> b;
g[a] = pos[b];
}
else if (op == 3) { // 交换 a b 两个部落
int a = 0, b = 0;
std::cin >> a >> b;
std::swap(idx[pos[a]], idx[pos[b]]);
std::swap(pos[a], pos[b]);
}
else { // 找 a 在那个部落
int a = 0;
std::cin >> a;
std::cout << idx[getfa(g[a])] << '\n';
}
}
}
1010
(算思维?)
感觉是乱搞搞过去的。
CODE
void solve()
{
int n = 0, m = 0;
std::cin >> n >> m;
std::vector d1(n, 0ll), d2(n, 0ll), d3(n, 0ll), d4(n, 0ll);
for (int i = 0; i < n; i++) {
i64 x = 0, y = 0;
std::cin >> x >> y;
d1[i] = x + y;
d2[i] = x + Inf - y;
d3[i] = Inf - x + Inf - y;
d4[i] = Inf - x + y;
}
std::sort(d1.begin(), d1.end());
std::sort(d2.begin(), d2.end());
std::sort(d3.begin(), d3.end());
std::sort(d4.begin(), d4.end());
i64 ans = 2 * Inf;
for (int i = 0; i < m; i++) {
i64 x = 0, y = 0;
std::cin >> x >> y;
ans = std::min( ans, std::max({ abs(d1.back() - (x + y)), abs(d2.back() - (x + Inf - y)), abs(d3.back() - (Inf - x + Inf - y)), abs(d4.back() - (Inf - x + y)) }));
}
std::cout << ans << '\n';
}
浙公网安备 33010602011771号