i = 4, lowbit(4) = 4
节点 4 管辖 [1,4]
i = 6, lowbit(6) = 2
节点 6 管辖 [5,6]
i = 7, lowbit(7) = 1
节点 7 管辖 [7,7]
i = 8, lowbit(8) = 8
节点 8 管辖 [1,8]
因此我们可以让:
mxi 表示节点 i 管辖区间内的最大值;
mni 表示节点 i 管辖区间内的最小值。
建树时,把每个位置 i 的身高加入所有包含它的树状数组节点中。
查询 [l,r] 时,从右端点 r 开始向左拆区间。
如果当前节点 r 管辖的整段区间:
[r−lowbit(r)+1,r]
完全在 [l,r] 内,就可以直接使用 mxr 和 mnr,然后跳过这一整段。
如果这段区间越过了左边界 l,就不能整段使用,只能单独处理当前位置 r,然后令 r 减 1。
这样就能把一个询问区间拆成若干个树状数组已经维护好的区间,再合并最大值和最小值。
以询问 [1,5] 为例:
[1,5] 可以拆成 [5,5] 和 [1,4]
其中:
节点 5 管辖 [5,5]
节点 4 管辖 [1,4]
所以只需要用这两个节点的信息,就能得到整个区间的最大值和最小值。
这道题没有修改操作,所有身高一开始就确定,所以用树状数组维护静态区间最值是可以的。
参考实现
#include<bits/stdc++.h>
using namespace std;
const int N = 50005;
const int INF = 1e9;
int n, q;
int a[N], mx[N], mn[N];
int lb(int x) {
return x & -x;
}
void add(int x, int v) {
for (int i = x; i <= n; i += lb(i)) {
mx[i] = max(mx[i], v);
mn[i] = min(mn[i], v);
}
}
int gmx(int l, int r) {
int res = 0;
while (r >= l) {
if (r - lb(r) + 1 >= l) {
res = max(res, mx[r]);
r -= lb(r);
} else {
res = max(res, a[r]);
r--;
}
}
return res;
}
int gmn(int l, int r) {
int res = INF;
while (r >= l) {
if (r - lb(r) + 1 >= l) {
res = min(res, mn[r]);
r -= lb(r);
} else {
res = min(res, a[r]);
r--;
}
}
return res;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) mn[i] = INF;
for (int i = 1; i <= n; i++) {
cin >> a[i];
add(i, a[i]);
}
while (q--) {
int l, r;
cin >> l >> r;
cout << gmx(l, r) - gmn(l, r) << '\n';
}
return 0;
}
题目要求各个松鼠在自己的子树内选糖果,且糖果各不相同。由于子树内的糖果对于子树根节点及以上的祖先节点是完全等价的,可以采用自底向上的方式计算方案数。
优先满足深度较大的节点需求。对于节点 u 的松鼠,它需要从 u 的子树中选出 Du 个糖果。当处理到 u 时,它所有的真后代(子树中除 u 外的节点)已经从这片区域中预定了它们需要的糖果。
设 u 的子树中初始总糖果数为 Wu,而 u 的所有真后代总共需要拿走 Ru 个糖果。真后代的选取限制在它们各自的子树内,只要数量足够,它们挑走特定的 Ru 个糖果后,剩下的 Wu−Ru 个糖果对于松鼠 u 而言没有任何限制,可以任选。
对每个节点 u,当前可用的糖果总数即为 Wu−Ru。松鼠 u 的局部方案数就是 (DuWu−Ru)。遍历整棵树时,如果出现子树总糖果数小于总需求数(即 Wu<Ru+Du),直接输出 0 并结束程序;否则将所有节点的局部方案数相乘即可。
组合数计算需要特别处理。Wu 的累加值可能达到 2×1014,而取模的质数 p=998244353 大于 106。根据卢卡斯定理,$\binom{n}{m} \equiv \binom{n \bmod p}{m \bmod p} \times \binom{n/p}{m/p} \pmod p$。因为 Du≤106<p,所以 Du/p=0,右侧第二项 (0n/p)=1。公式直接退化为计算 (Dunmodp)(modp)。将可用糖果数量对 p 取模,若取模后数值小于 Du 则返回 0,否则在 O(Du) 时间内通过展开分子分母计算组合数。
参考实现
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = 998244353;
const int MX = 200005;
vector<int> adj[MX];
ll c[MX], d[MX], sc[MX], sd[MX];
ll ans = 1;
ll qpw(ll a, ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return r;
}
ll inv(ll a) {
return qpw(a, MOD - 2);
}
ll get(ll n, ll k) {
n %= MOD;
if (n < k) return 0;
ll num = 1, den = 1;
for (ll i = 0; i < k; i++) {
num = num * (n - i) % MOD;
den = den * (i + 1) % MOD;
}
return num * inv(den) % MOD;
}
void dfs(int u) {
sc[u] = c[u];
sd[u] = d[u];
for (int v : adj[u]) {
dfs(v);
sc[u] += sc[v];
sd[u] += sd[v];
}
if (sc[u] < sd[u]) {
cout << 0 << "\n";
exit(0);
}
ll rem = sc[u] - sd[u] + d[u];
ans = ans * get(rem, d[u]) % MOD;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
for (int i = 2; i <= n; i++) {
int p;
cin >> p;
adj[p].push_back(i);
}
for (int i = 1; i <= n; i++) cin >> c[i];
for (int i = 1; i <= n; i++) cin >> d[i];
dfs(1);
cout << ans << "\n";
return 0;
}
要把 4 个变量的枚举变成 2 个,突破口在路径的结构上。
我们要走的路径是 1→A→B→C→D→1。它是一个首尾相接的对称环。如果在中间切一刀,把 B 和 C 当作桥墩确定下来,整个过程就被劈成了左半边 1→A→B 和右半边 C→D→1。
一旦 B 被外层循环固定,给它找前驱 A 的任务只和 B 与 1 有关;一旦 C 固定,找后继 D 的任务只和 C 与 1 有关。这两半是解耦的,把它们拆开分别处理,这就是折半枚举的核心动机。
于是思路变成了:双重循环枚举 B 和 C。对于每一个可能作为桥墩的节点,提前帮它把所有能作为 A 或者 D 的候选点找出来。
为了让总分最大,预处理时可以直接挑分数最大的那个候选点吗?
题目规定 4 个点必须互不相同,直接贪心选最大的容易翻车。假设预处理时只给每个点记录了分数第一的候选点,当外层循环枚举到具体的 B 和 C 时,万一 B 记录的那个最强 A 恰好就是 C,或者恰好和 D 撞了位置,唯一的备选就失效了,这条路直接作废。
算一下具体的排斥情况:给 B 找 A 的时候,A 需要避让的“竞争者”最多只有三个,分别是当前的 B、当前的 C、以及 C 选出来的 D。
在最极端的冲突下,分数第一大的备选被 C 占了,第二大的备选被 D 占了,那么第三大的备选就一定是个自由身。也就是说,在预处理阶段,只要把满足连通条件的备选点按分数从大到小排个序,只存前 3 个,就足以应付所有位置重合的极端情况。
通过维护前 3 大这个常数级的操作,既完美避开了去重的雷区,又不需要增加多余的循环层数,将复杂度彻底锁在了 O(N2)。
参考实现
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n, m, k;
ll val[2505];
vector<int> g[2505];
int ok[2505][2505];
int dis[2505];
int bst[2505][5];
void bfs(int s) {
for (int i = 1; i <= n; ++i) {
dis[i] = -1;
}
dis[s] = 0;
queue<int> q;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
if (dis[u] == k + 1) continue;
for (int v : g[u]) {
if (dis[v] == -1) {
dis[v] = dis[u] + 1;
q.push(v);
}
}
}
for (int i = 1; i <= n; ++i) {
if (dis[i] != -1 && dis[i] <= k + 1) {
ok[s][i] = 1;
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> k;
for (int i = 2; i <= n; ++i) {
cin >> val[i];
}
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; ++i) {
bfs(i);
}
for (int i = 2; i <= n; ++i) {
vector<int> tmp;
for (int j = 2; j <= n; ++j) {
if (i != j && ok[1][j] && ok[j][i]) {
tmp.push_back(j);
}
}
sort(tmp.begin(), tmp.end(), [&](int x, int y) {
return val[x] > val[y];
});
int sz = tmp.size();
for (int j = 0; j < 3 && j < sz; ++j) {
bst[i][j] = tmp[j];
}
}
ll ans = 0;
for (int i = 2; i <= n; ++i) {
for (int j = 2; j <= n; ++j) {
if (i == j || !ok[i][j]) continue;
for (int x = 0; x < 3; ++x) {
int a = bst[i][x];
if (!a || a == j) continue;
for (int y = 0; y < 3; ++y) {
int d = bst[j][y];
if (!d || d == i || d == a) continue;
ans = max(ans, val[a] + val[i] + val[j] + val[d]);
}
}
}
}
cout << ans << '\n';
return 0;
}
定义状态 dp[l][r][p] 表示在区间 [l,r] 内,准确使用 p 个乘号所能获得的最大结果。
当区间长度大于 1 时,枚举一个分割点 m 将当前区间分为 [l,m] 和 [m+1,r]。假设左半部分分配了 c 个乘号,那么两部分合并的运算方式有两种可能:
若两段区间由加号连接,那么右半部分需要使用 p - c 个乘号,此时结果为左右部分最大值之和。
若两段区间由乘号连接,那么连接本身消耗了一个乘号,右半部分就只能使用 p - 1 - c 个乘号,此时结果为左右部分最大值的乘积。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
ll dp[20][20][20];
ll a[20];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 20; j++) {
for (int p = 0; p < 20; p++) {
dp[i][j][p] = -1;
}
}
}
for (int i = 1; i <= n; i++) {
dp[i][i][0] = a[i];
}
for (int len = 2; len <= n; len++) {
for (int l = 1; l <= n - len + 1; l++) {
int r = l + len - 1;
for (int m = l; m < r; m++) {
for (int p = 0; p <= k && p <= r - l; p++) {
for (int c = 0; c <= p; c++) {
if (dp[l][m][c] != -1 && dp[m + 1][r][p - c] != -1) {
dp[l][r][p] = max(dp[l][r][p], dp[l][m][c] + dp[m + 1][r][p - c]);
}
}
if (p > 0) {
for (int c = 0; c <= p - 1; c++) {
if (dp[l][m][c] != -1 && dp[m + 1][r][p - 1 - c] != -1) {
dp[l][r][p] = max(dp[l][r][p], dp[l][m][c] * dp[m + 1][r][p - 1 - c]);
}
}
}
}
}
}
}
cout << dp[1][n][k] << "\n";
return 0;
}
给定 m 个完全相同的乒乓球和 n 个不同的盒子。第 i 个盒子最多只能放入 ai 个乒乓球。求将这 m 个球全部分配到 n 个盒子中的合法方案数。由于结果可能很大,输出方案数对 1000000007 取模的结果。
假设本题的数据范围为 n,m≤2000,0≤ai≤m。输入的第一行为 n 和 m,第二行为 n 个整数 a1,a2,…,an。输出一个整数表示取模后的方案数。
为了让最终“人数最少的小组人数最大”,在有多个现有小组都以 x−1 结尾时,应该优先把 x 接到当前人数最少的那一组后面。这是因为短板决定了最终的答案,把新数字分配给当前最短的小组,能最有效地拉长这个短板。如果没有以 x−1 结尾的小组,说明现有的组都无法容纳 x,此时 x 只能自己新开一组,初始人数为 1。
为了高效地实现这个挑选过程,需要记录每一个小组的结尾数值以及该小组的当前人数。可以使用一个映射,它的键是小组的结尾数值,对应的值是一个最小堆(优先队列)。最小堆里存放所有以该数值结尾的小组的人数。这样每次处理数字 x 时,只需看 x−1 对应的最小堆是否为空:
如果不为空,取出堆顶(也就是最短的小组人数),将其加 1 后放入 x 对应的最小堆中。
如果为空,说明无法拼接,直接在 x 对应的最小堆中插入 1。
把所有数字处理完后,遍历整个结构,找出所有现存小组人数的最小值即可。
参考实现
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int a[100005];
map<int, priority_queue<int, vector<int>, greater<int>>> mp;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a, a + n);
for (int i = 0; i < n; i++) {
int x = a[i];
if (mp[x - 1].empty()) {
mp[x].push(1);
} else {
int len = mp[x - 1].top();
mp[x - 1].pop();
mp[x].push(len + 1);
}
}
int ans = 2e9;
for (auto &p : mp) {
if (!p.second.empty()) {
ans = min(ans, p.second.top());
}
}
cout << ans << "\n";
return 0;
}
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
bool f[100005];
void mkk() {
fill(f + 2, f + 100005, true);
for (int i = 2; i * i <= 100000; i++) {
if (f[i]) {
for (int j = i * i; j <= 100000; j += i) f[j] = false;
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
mkk();
int t;
cin >> t;
while (t--) {
int h;
cin >> h;
int ans = 1e9;
for (int k = 0; k <= 17; k++) {
int p2 = 1 << k;
if (h + 1 == p2) {
ans = min(ans, k);
}
int x = h + 1 - p2;
if (x >= 2 && x <= h && f[x]) {
ans = min(ans, k + 1);
}
}
if (ans > 1e8) cout << -1 << "\n";
else cout << ans << "\n";
}
return 0;
}