P11673 [USACO25JAN] Median Heap G 题解
题目传送门
题目大意
对于一颗完全二叉树,定义其节点 \(u\) 的
- 左儿子 \(ls=u\times2\),右儿子 \(rs=u\times2+1\)
- 父亲为 \(\lfloor \frac u2\rfloor\)
- 置换操作:将 \(u\) 的值换为 \(\{u,\) \(ls\)(如果有), \(rs\)(如果有)\(\}\) 的中位数,代价为 \(\text0\)
- 修改操作:将 \(u\) 的值修改为任意数,代价为 \(c_u\)
- “近似中位数”:从最后一个节点 \(n\) 开始向前执行,若此节点非叶子节点,则进行置换操作,最后节点 \(1\) (根)的值即为近似中位数
现在给你这颗树的初始权值,和修改每个点的代价。
再给出一个询问列表 \(m_1,m_2,\cdots,m_q\),对于每个询问,回答使树的近似中位数为 \(m_i\) 所需要的最小代价。
部分分
首先我们来看 \(n,q\leq1000\) 的部分。
定义函数
定义dp数组
转移方程
其中 \(\text{cost}(k)\) 代表将 \(a_u\) 修改为 小于/等于/大于 \(m_i\) 的代价
具体地,
vector<int> cost = {c[u], c[u], c[u]};
cost[f[u]] = 0;
dp代码实现:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define med(a, b, c) (a ^ b ^ c ^ min({a, b, c}) ^ max({a, b, c}))
#define p(m, x) (x < m ? 0 : x == m ? 1 : 2)
#define ls u << 1
#define rs u << 1 | 1
const int N = 2e5 + 5, M = 4e5 + 5;
const int INF = 1e18;
int n, q;
vector<int> a(N), c(N);
vector<int> ans(M);
vector<int> f(N); // f[i]=p(m,i)
int dp[M][3];
// 更新使节点u的近似中位数为m的最小代价
void update(int u)
{
// 将 a[u] 修改为一个 <m =m >m 的数的代价
vector<int> cost = {c[u], c[u], c[u]};
cost[f[u]] = 0;
if (rs >= n) { // 叶子结点
for (int i = 1; i <= 3; ++i) dp[u][i] = cost[i];
return ;
}
dp[u][0] = dp[u][1] = dp[u][2] = INF;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
int mod = med(i, j, k);
dp[u][mod] = min(dp[u][mod], dp[ls][i] + dp[rs][j] + cost[k]);
}
}
}
}
对于每个询问,我们都
for (int i = 1; i <= q; ++i) {
int m;
cin >> m;
for (int j = n; j; --j) {
f[j] = p(m, a[j]);
update(j);
}
cout << dp[1][1] << '\n';
}
注意这里查看 f[j] 的修改情况时一定要倒序!!!这样才能保证从叶子节点开始修改,否则你先把父亲更新了结果儿子还没更等于没更,包WA的。
部分分优化(满分)
我们注意到,可以把问询离线下来。
把问询从小到大排序后,我们再看上码,可以发现每次增大询问值后,并不是所有的 \(f[j]\) 都需要修改。
又因为只有 \(f[j]\) 修改才会造成 dp 改变,只需要每轮查看哪些 \(f[j]\) 被修改了,针对它们更新从这个节点j不断往上更新父亲的 dp,因为显然除了父亲这个节点无法更改任何其他 dp。
显然 f[j] 是越修越大的,因此最多修改2次(0->1,1->2)。
那么,现在我们只需寻找一种高效的方式,每次精准查找需要修改的 \(f[j]\),显然不能遍历,否则直接给你炸成 Time Limit Enough
这个时候,我们想到,可以开一个 \(pos[val]\),记录值为 \(a_u=val\) 的 下标 \(u\),每次问询值为 \(m\) 时,就遍历 \(pos[m]\),将其中所有 \(f[j]\) 修改为 \(1\),注意一定要将 \(pos[m]\) 逆序,使其中内容从 单调递增变为单调递减.然后更新dp。更完后,下一个 \(m\) 肯定比现在大,因此我们可以预测这些点下一轮的 \(f[j]\) 必定为 \(2\) ,所以可以在现在这个循环提前修改。
代码实现:
void modify(int u, int state) // 要修的 f[u] 和修成的值
{
f[u] = state;
while (u) {
update(u);
u >>= 1;
}
}
int main()
{
// existing code...
// 离散化
for (int i = 1; i <= n; ++i) {
pos[a[i]].push_back(i);
}
for (auto x: query) { // 处理问询,query为排序+离散化后问询数组
reverse(pos[x].begin(), pos[x].end());
for (auto i: pos[x]) modify(i, 1);
ans[x] = dp[1][1];
for (auto i: pos[x]) modify(i, 2);
}
for (int i = 1; i <= q; ++i) {
cout << ans[m[i]] << '\n';
}
AC 代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ls u << 1
#define rs u << 1 | 1
const int N = 2e5 + 5, M = 4e5 + 5;
const int INF = 1e18;
int med(int a, int b, int c) { return a ^ b ^ c ^ min({a, b, c}) ^ max({a, b, c}); }
int p(int m, int x) { return m < x ? 0 : m == x ? 1 : 2; }
int n, q;
vector<int> a(N), c(N);
vector<int> ans(M);
vector<int> f(M); // f[i]=p(m,i)
int dp[M][3];
// 更新使节点u的近似中位数为m的最小代价
void update(int u)
{
// 将 a[u] 修改为一个 <m =m >m 的数的代价
vector<int> cost = {c[u], c[u], c[u]};
cost[f[u]] = 0;
if (ls > n) { // 叶子结点
for (int i = 0; i < 3; ++i) dp[u][i] = cost[i];
return ;
}
dp[u][0] = dp[u][1] = dp[u][2] = INF;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
int mod = med(i, j, k);
dp[u][mod] = min(dp[u][mod], dp[ls][i] + dp[rs][j] + cost[k]);
}
}
}
}
// update the dp-tree's change due to
// f[u]'s change, which implies a change in cost_u[0/1/2]
void modify(int u, int state)
{
f[u] = state;
while (u) {
update(u);
u >>= 1;
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j < 3; ++j) {
dp[i][j] = INF;
}
}
vector<int> all;
for (int i = 1; i <= n; ++i) {
cin >> a[i] >> c[i];
all.push_back(a[i]);
}
cin >> q;
vector<int> m(q + 1);
for (int i = 1; i <= q; ++i) {
cin >> m[i];
all.push_back(m[i]);
}
// printf("a: ");
// for (int i = 1; i <= n; ++i) printf("%lld ", a[i]);
// printf("\n");
// printf("m: ");
// for (int i = 1; i <= q; ++i) printf("%lld ", m[i]);
// printf("\n");
// 离散化
sort(all.begin(), all.end());
all.erase(unique(all.begin(), all.end()), all.end());
int cnt = 1;
map<int, int> corr;
for (auto i: all) {
corr[i] = cnt++;
}
map<int, vector<int>> pos;
for (int i = 1; i <= n; ++i) {
a[i] = corr[a[i]];
pos[a[i]].push_back(i);
}
for (int i = 1; i <= q; ++i) {
m[i] = corr[m[i]];
}
// printf("a: ");
// for (int i = 1; i <= n; ++i) printf("%lld ", a[i]);
// printf("\n");
// printf("m: ");
// for (int i = 1; i <= q; ++i) printf("%lld ", m[i]);
// printf("\n");
vector<int> query;
for (int i = 1; i <= q; ++i) {
query.push_back(m[i]);
}
sort(query.begin(), query.end());
query.erase(unique(query.begin(), query.end()), query.end());
// initialize dp to all '>'s
for (int i = n; i > 0; --i) {
// modify(i, 0); unnecessary cuz it's alr all 0s
update(i);
}
// for (int i = 1; i < cnt; ++i) {
// printf("dp[%lld]: {%lld, %lld, %lld}\n", i, dp[i][0], dp[i][1], dp[i][2]);
// }
for (int x = 1; x < cnt; ++x) {
reverse(pos[x].begin(), pos[x].end());
for (auto i: pos[x]) {
modify(i, 1);
}
ans[x] = dp[1][1];
for (auto i: pos[x]) {
modify(i, 2);
}
// for (int i = 1; i < cnt; ++i) {
// printf("dp[%lld]: {%lld, %lld, %lld}\n", i, dp[i][0], dp[i][1], dp[i][2]);
// }
}
for (int i = 1; i <= q; ++i) {
cout << ans[m[i]] << '\n';
}
return 0;
}
进食后人
在执行修改dp操作时,要把 \(a\) 和 \(m\) 所有元素都遍历到,确保大小关系的正确性,防止有数值未更新。
这点尤其坑,因为如果你只遍历 \(m\) 的话,样例是 可以过 的,并且总共能 A掉 \(3\)个点,喜提\(16\text{pts}\)。
一定要初始化问询为0。
不仅如此,这个初始化必须逆序。(我卡了大概10min吧

浙公网安备 33010602011771号