题目传送门
题目大意
对于一颗完全二叉树,定义其节点 u u u 的
- 左儿子 l s = u × 2 ls=u\times2 ls=u×2,右儿子 r s = u × 2 + 1 rs=u\times2+1 rs=u×2+1
- 父亲为 ⌊ u 2 ⌋ \lfloor \frac u2\rfloor ⌊2u⌋
- 置换操作:将 u u u 的值换为 { u , \{u, {u, l s ls ls(如果有), r s rs rs(如果有) } \} } 的中位数,代价为 0 \text0 0
- 修改操作:将 u u u 的值修改为任意数,代价为 c u c_u cu
- “近似中位数”:从最后一个节点 n n n 开始向前执行,若此节点非叶子节点,则进行置换操作,最后节点 1 1 1 (根)的值即为近似中位数
现在给你这颗树的初始权值,和修改每个点的代价。
再给出一个询问列表
m
1
,
m
2
,
⋯
,
m
q
m_1,m_2,\cdots,m_q
m1,m2,⋯,mq,对于每个询问,回答使树的近似中位数为
m
i
m_i
mi 所需要的最小代价。
部分分
首先我们来看
n
,
q
≤
1000
n,q\leq1000
n,q≤1000 的部分。
定义函数
p
(
m
i
,
a
u
)
=
{
0
m
i
<
a
u
1
m
i
=
a
u
2
m
i
>
a
u
并维护每个询问都刷新的数组
f
u
=
p
(
m
i
,
a
u
)
,此处
m
i
为全局变量
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 为全局变量
p(mi,au)=⎩
⎨
⎧012mi<aumi=aumi>au并维护每个询问都刷新的数组 fu=p(mi,au),此处 mi为全局变量
定义dp数组
d
p
[
u
]
[
case
]
=
修改节点
u
使其 近似中位数
{
case
=
0
<
m
i
case
=
1
=
m
i
case
=
2
>
m
i
的最小代价
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][case]=修改节点 u 使其 近似中位数⎩
⎨
⎧case=0case=1case=2< mi= mi> mi的最小代价
转移方程
d
p
[
u
]
[
s
]
=
min
med
{
i
,
j
,
k
}
=
s
d
p
[
l
s
]
[
i
]
+
d
p
[
r
s
]
[
j
]
+
cost
[
k
]
dp[u][s]=\min_{\text{med}\{i,j,k\}=s}{dp[ls][i]+dp[rs][j]+\text{cost}[k]}
dp[u][s]=med{i,j,k}=smindp[ls][i]+dp[rs][j]+cost[k]
其中
cost
(
k
)
\text{cost}(k)
cost(k) 代表将
a
u
a_u
au 修改为 小于/等于/大于
m
i
m_i
mi 的代价
具体地,
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]
f[j] 都需要修改。
又因为只有
f
[
j
]
f[j]
f[j] 修改才会造成 dp 改变,只需要每轮查看哪些
f
[
j
]
f[j]
f[j] 被修改了,针对它们更新从这个节点j不断往上更新父亲的 dp,因为显然除了父亲这个节点无法更改任何其他 dp。
显然 f[j] 是越修越大的,因此最多修改2次(0->1,1->2)。
那么,现在我们只需寻找一种高效的方式,每次精准查找需要修改的
f
[
j
]
f[j]
f[j],显然不能遍历,否则直接给你炸成 Time Limit Enough
这个时候,我们想到,可以开一个
p
o
s
[
v
a
l
]
pos[val]
pos[val],记录值为
a
u
=
v
a
l
a_u=val
au=val 的 下标
u
u
u,每次问询值为
m
m
m 时,就遍历
p
o
s
[
m
]
pos[m]
pos[m],将其中所有
f
[
j
]
f[j]
f[j] 修改为
1
1
1,注意一定要将
p
o
s
[
m
]
pos[m]
pos[m] 逆序,使其中内容从 单调递增变为单调递减.然后更新dp。更完后,下一个
m
m
m 肯定比现在大,因此我们可以预测这些点下一轮的
f
[
j
]
f[j]
f[j] 必定为
2
2
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
a
a 和
m
m
m 所有元素都遍历到,确保大小关系的正确性,防止有数值未更新。
这点尤其坑,因为如果你只遍历
m
m
m 的话,样例是 可以过 的,并且总共能 A掉
3
3
3个点,喜提
16
pts
16\text{pts}
16pts。
一定要初始化问询为0。
不仅如此,这个初始化必须逆序。(我卡了大概10min吧
1227



被折叠的 条评论
为什么被折叠?



