算法能力提升之树形结构-(线段树)
今天给大家带来的这道题是对于线段树的应用,这道题可以帮助大家更好地理解线段树的本质特征,理解为什么线段树可以利用O(logn)的时间复杂度来实现大部分题目的优化,这可比暴力接发来的更为关键,可以帮助大家更好地拿到分数。
问题描述
给定一个长度为 N 的数列 A1,A2,⋯,AN 。现在小蓝想通过若干次操作将 这个数列中每个数字清零。
每次操作小蓝可以选择以下两种之一:
- 选择一个大于 0 的整数, 将它减去 1 ;
- 选择连续 K 个大于 0 的整数, 将它们各减去 1 。
小蓝最少经过几次操作可以将整个数列清零?
输入格式
输入第一行包含两个整数 N 和 K 。
第二行包含 N 个整数 AA1,A2,⋯,AN 。
输出格式
输出一个整数表示答案。
输入案例:
4 2
1 2 3 4
输出案例:
6
代码部分:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 10;
ll lz[N * 4], t[N * 4], a[N], n, k, ans;void pushup(ll o)
{t[o] = min(t[o << 1], t[o << 1 | 1]);
}void build(ll s = 1, ll e = n, ll o = 1)
{if (s == e){t[o] = a[s];return;}ll mid = s + e >> 1;build(s, mid, o << 1);build(mid + 1, e, o << 1 | 1);pushup(o);
}void pushdown(ll s, ll e, ll o)
{if (lz[o]){ll ls = o << 1, rs = o << 1 | 1, mid = s + e >> 1;t[ls] -= lz[o], lz[ls] += lz[o];t[rs] -= lz[o], lz[rs] += lz[o];lz[o] = 0;}
}void update(ll l, ll r, ll v, ll s = 1, ll e = n, ll o = 1)
{if (l <= s && e <= r){t[o] -= v;lz[o] += v;return;}ll mid = s + e >> 1;pushdown(s, e, o);if (mid >= l)update(l, r, v, s, mid, o << 1);if (mid + 1 <= r)update(l, r, v, mid + 1, e, o << 1 | 1);pushup(o);
}ll query(ll l, ll r, ll s = 1, ll e = n, ll o = 1)
{if (l <= s && e <= r)return t[o];ll mid = s + e >> 1;pushdown(s, e, o);ll res = 1e9;if (mid >= l)res = min(res, query(l, r, s, mid, o << 1));if (mid + 1 <= r)res = min(res, query(l, r, mid + 1, e, o << 1 | 1));return res;
}int main()
{ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);cin >> n >> k;for (ll i = 1; i <= n; ++i)cin >> a[i];build();for (ll i = 1; i <= n; ++i){if (i + k - 1 <= n) // 还可以执行k操作{// 询问得到最小值ll x = query(i, i + k - 1);if (x != 0){ans += x;update(i, i + k - 1, x);}}ll y = query(i, i);if (y != 0){ans += y;update(i, i, y);}}cout << ans;
}
注意点⚠️:
1.
先明确问题的核心:两种操作的最优策略
题目要求用两种操作将数列清零,求最少操作次数:
- 单元素操作:选 1 个正数减 1,代价 1 次(适合零散的、无法批量处理的元素);
- 连续 K 元素操作:选连续 K 个正数各减 1,代价 1 次(适合批量处理,能大幅减少操作次数,是优化核心)。
最优策略推导
要让操作次数最少,必须最大化 “连续 K 元素操作” 的使用频率—— 因为一次批量操作能顶 K 次单元素操作。具体策略可类比 “分层处理”:
- 从左到右遍历数列,对每个位置
i
:- 若
i
能作为 “连续 K 元素操作” 的起点(即i + K - 1 ≤ n
),先计算当前区间[i, i+K-1]
的最小值x
—— 这意味着能对该区间执行x
次批量操作(每次批量操作会让区间内所有元素减 1,x
次后至少有一个元素变为 0,无法再批量操作); - 执行
x
次批量操作后,将区间内元素减去x
,操作次数累加x
; - 对位置
i
剩余的非零值(无法再批量处理,因为后续位置超出 K 范围),执行y
次单元素操作,操作次数累加y
,并将i
清零。
- 若
关键痛点:在遍历过程中,需要频繁执行两个操作:
- 查询区间
[i, i+K-1]
的最小值(确定能执行多少次批量操作); - 对区间
[i, i+K-1]
或单点[i,i]
执行 “整体减 x” 的更新(模拟批量 / 单元素操作后的数值变化)。
这两个操作恰好是线段树的核心优势场景—— 线段树能在O(log n)
时间内高效完成 “区间查询极值” 和 “区间范围更新”,完美匹配问题需求。
2.关键代码部分的解释:
for (ll i = 1; i <= n; ++i) {if (i + k - 1 <= n) // 可执行批量操作{ll x = query(i, i + k - 1); // 线段树查区间最小值:确定最多x次批量操作if (x != 0){ans += x;update(i, i + k - 1, x); // 线段树区间更新:区间内所有元素减x}}ll y = query(i, i); // 线段树查单点值:剩余无法批量处理的部分if (y != 0){ans += y;update(i, i, y); // 线段树单点更新:清零当前元素}
}
注意query(i,i)不要理解为是时间复杂度为O(n),仍然是log(n)。
好了,今天的分享就到这里,希望能对你有所帮助。