P11673 [USACO25JAN] Median Heap G 题解

题目传送门

洛谷P11673

题目大意

对于一颗完全二叉树,定义其节点 \(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\) 的部分。
定义函数

\[p(m_i,a_u)=\begin{cases}0&m_i<a_u\\1&m_i=a_u\\2&m_i>a_u\end{cases} \\并维护每个询问都刷新的数组\ f_u=p(m_i,a_u),此处\ m_i 为全局变量 \]

定义dp数组

\[dp[u][\text{case}]=修改节点\ u\ 使其\ 近似中位数\begin{cases}\text{case}=0&<\ m_i\\\text{case}=1&=\ m_i\\\text{case}=2&>\ m_i\end{cases}的最小代价 \]

转移方程

\[dp[u][s]=\min_{\text{med}\{i,j,k\}=s}{dp[ls][i]+dp[rs][j]+\text{cost}[k]} \]

其中 \(\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吧

posted @ 2025-08-28 11:41  peter_code  阅读(18)  评论(0)    收藏  举报