ABC461-①贪心②前缀和+双指针③树状数组
ABC461-①贪心②前缀和+双指针③树状数组
目录
C-贪心
题意
有 N 个宝石。
第 i 个宝石:
- 颜色为
Ci - 价值为
Vi
需要恰好选择 K 个宝石,并且满足:
- 选出的宝石颜色种类数不少于
M
求能够获得的最大总价值。
数据范围:
1 ≤ M ≤ K ≤ N ≤ 2×10^51 ≤ Vi ≤ 10^9
思路分析
将同一种颜色的宝石按照价值从大到小排序:
v1 ≥ v2 ≥ v3 ≥ ...如果最终方案使用了这种颜色,那么一定可以选择:
v1作为该颜色的代表宝石。
因为如果选择的是:
v2那么直接换成:
v1颜色数不会减少,而总价值一定不会变小。
因此:
- 每种颜色价值最大的宝石称为 颜色代表
- 其余宝石称为 普通宝石
题目要求:
至少 M 种颜色因此必须先选择 M 个颜色代表。
设所有颜色代表的价值为:
a1,a2,a3,...显然应该选择价值最大的 M 个颜色代表。
否则把较小的颜色代表替换成更大的颜色代表一定更优。
已经选了:
M 个颜色代表还需要选择:
K-M 个宝石此时颜色数要求已经满足。
因此剩余位置只需要尽量提高价值即可。
剩余可选宝石包括:
- 所有普通宝石
- 未被选作颜色代表的颜色代表
即:
tail += r[1:]tail += top[M:]然后从这些宝石中选出价值最大的:
K-M 个即可。
正确性证明
设某个颜色被选中。
若没有选择该颜色价值最大的宝石,而选择了价值较小的宝石:
v1 > vx那么将其替换为:
v1颜色种类数不会改变,而总价值增大。
因此最优解一定存在一种形式:
每个被使用的颜色都选择其最大价值宝石为了满足至少 M 种颜色,显然应选择价值最大的 M 个颜色代表。
之后颜色要求已经满足。
剩余的 K-M 个位置不再影响颜色数,因此直接选择剩余宝石中价值最大的 K-M 个即可。
故该贪心策略正确。
算法步骤
- 按颜色分组。
- 每组按价值降序排序。
- 取每组最大值放入
top。 - 其余宝石放入
tail。 - 将
top降序排序。 - 选择最大的
M个颜色代表。 - 将剩余颜色代表加入
tail。 - 从
tail中选择最大的K-M个宝石。 - 输出总价值。
复杂度分析
设宝石总数为 N。
排序总复杂度:
O(N \log N)空间复杂度:
O(N)能够通过:
N ≤ 2×10^5n,k,m = map(int,input().split())t = [[] for _ in range(n+1)]for i in range(n): c,v = map(int,input().split()) t[c].append(v)top = []tail = []for r in t: if len(r) > 0: r.sort(reverse = True) top.append(r[0]) tail += r[1:]top.sort(reverse = True)tail += top[m:]tail.sort(reverse = True)print(sum(top[:m]) + sum(tail[:k - m]))D-前缀和+双指针
题意
给定一个 H × W 的 01 矩阵。
求有多少个矩形满足:
- 矩形边界与网格对齐;
- 矩形内部所有数字之和恰好等于
K。
即统计满足:
且
的矩形数量。
思路分析
直接枚举矩形四条边:
上边界下边界左边界右边界复杂度:
当
时显然无法通过。
第一步:固定上下边界
先对每一列做纵向前缀和。
设:
a[i][j]表示第 j 列前 i 行中 1 的个数。
则:
a[d][j] - a[u-1][j]表示:
第 j 列从第 u 行到第 d 行之间的 1 的数量固定:
上边界 u下边界 d后,
每一列的贡献变成:
于是问题转化为:
一个长度为 W 的非负数组中,求和为 K 的连续子数组个数。
第二步:列前缀和
构造前缀和:
b[0]=0
b[j]=b[j-1]+col_j则区间:
的和为:
于是问题变成:
统计满足
的前缀和对数。
第三步:双指针
注意:
矩阵元素只有:
0 或 1因此:
col_j ≥ 0从而:
b[j]单调不减。
对于每个左端点 l:
维护两个指针:
r1
最小满足:
的位置。
r2
最小满足:
的位置。
则:
[r1,r2)中的所有位置都满足:
贡献:
即可。
子过程
定义:
subp(b,k)统计:
的方案数。
由于:
b 单调不减双指针总共只移动:
次。
总复杂度
枚举上下边界:
每次统计:
因此总复杂度:
复杂度分析
时间复杂度:
空间复杂度:
当:
时可以通过。
def subp(a, k): n = len(a) r1 = 0 r2 = 0 res = 0 for l in range(n - 1): r1 = max(r1, l + 1) r2 = max(r2, l + 1) while r1 < n and a[r1] - a[l] < k: r1 += 1 while r2 < n and a[r2] - a[l] <= k: r2 += 1 res += r2 - r1 return resH, W, K = map(int, input().split())a = [[0] * (W + 1) for _ in range(H + 1)]for i in range(1, H + 1): s = input().strip() for j in range(1, W + 1): if s[j - 1] == '1': a[i][j] = 1for i in range(1, H + 1): for j in range(1, W + 1): a[i][j] += a[i - 1][j]ans = 0for u in range(1, H + 1): for d in range(u, H + 1): b = [0] * (W + 1) for j in range(1, W + 1): b[j] = b[j - 1] + (a[d][j] - a[u - 1][j]) ans += subp(b, K)print(ans)h, w, k = map(int, input().split())grid = [input().strip() for _ in range(h)]pre = [[0] * w for _ in range(h + 1)]for i in range(h): for j in range(w): pre[i + 1][j] = pre[i][j] + (1 if grid[i][j] == '1' else 0)ans = 0for r1 in range(h): for r2 in range(r1, h): col_sum = [0] * w for j in range(w): col_sum[j] = pre[r2 + 1][j] - pre[r1][j] pref = 0 cnt = {0: 1} for val in col_sum: pref += val if pref - k in cnt: ans += cnt[pref - k] cnt[pref] = cnt.get(pref, 0) + 1print(ans)E-树状数组
题意
有一个 N × N 的网格,初始时所有格子均为白色。
进行 Q 次操作:
操作 1
1 R将第 R 行的所有格子染成黑色。
操作 2
2 C将第 C 列的所有格子染成白色。
每次操作结束后,输出当前网格中的黑色格子数量。
数据范围
1 ≤ N,Q ≤ 3×10^5显然不能真的维护整个矩阵。
思路分析
对于任意一个格子:
(row , col)最终颜色只取决于:
最近一次涉及它的操作即:
- 最近一次给所在行染黑
- 最近一次给所在列染白
谁发生得更晚。
时间轴思想
将每次操作编号:
1,2,3,...,Q对于一行 R:
记录:
last_row[R]表示最近一次:
操作 1出现的位置。
对于一列 C:
记录:
last_col[C]表示最近一次:
操作 2出现的位置。
对于格子:
(R,C)若:
last_row[R] > last_col[C]则该格子为黑色。
否则为白色。
维护黑色格子数量
设当前黑格数量为:
b操作 1:整行染黑
假设:
1 R发生在时间:
i第一次出现
如果此前从未染黑过该行:
last_row[R] == -1则整行直接变黑:
b += N不是第一次
旧时间:
old = last_row[R]原来该行贡献的黑格数为:
N - (在 old 之后被染白的列)更新后:
N - (在 i 之后被染白的列)两者差值就是新增黑格数。
设:
kind=old 与 i 之间出现的列操作数量则:
b += kind操作 2:整列染白
同理:
2 C若是第一次出现:
last_col[C] = 0表示初始白色状态。
设旧时间:
old = last_col[C]统计:
old 与 i 之间出现的行操作数量记为:
kind这些格子原本是黑色,
现在被染白。
因此:
b -= kind如何统计区间内操作数?
需要支持:
统计某类操作在时间区间内出现次数例如:
行操作出现于:
3,7,10,...要查询:
[old , i)之间有多少个。
这是典型:
单点修改前缀和统计使用树状数组维护即可。
树状数组维护
建立两个 Fenwick Tree:
f[0]记录:
行操作出现的位置。
f[1]记录:
列操作出现的位置。
查询:
f[t].sum(l,r)即可得到:
[l,r)区间内该类操作数量。
算法流程
维护:
last_rowlast_col以及:
b当前黑格数。
对于操作:
行染黑
if first: b += Nelse: kind = 区间内列操作数量 b += kind列染白
kind = 区间内行操作数量b -= kind更新:
lastFenwick输出答案。
正确性证明
对于任意格子:
(r,c)颜色仅由:
最近一次行染黑最近一次列染白决定。
若最近的是行操作:
last_row[r] > last_col[c]则该格子黑色。
否则白色。
一次新的行操作:
只会影响:
该行中那些最近一次列操作发生在:
old ~ now之间的格子。
数量正是区间中的列操作种类数。
因此:
b += kind正确。
同理列操作:
会把对应区间产生的黑格全部消除。
因此:
b -= kind正确。
故算法正确。
复杂度分析
每次操作:
- Fenwick 查询一次
- Fenwick 修改一次
均为:
O(\log Q)总复杂度:
O(Q\log Q)空间复杂度:
O(N+Q)class FenwickTree: def __init__(self, n): self.n = n self.bit = [0] * (n + 1) def add(self, idx, val): idx += 1 while idx <= self.n: self.bit[idx] += val idx += idx & -idx def sum_prefix(self, r): res = 0 while r > 0: res += self.bit[r] r -= r & -r return res def sum(self, l, r): return self.sum_prefix(r) - self.sum_prefix(l)N, Q = map(int, input().split())# f[0]:记录行染黑操作出现的时间点# f[1]:记录列染白操作出现的时间点f = [FenwickTree(Q + 1) for _ in range(2)]# 当前黑色格子数量b = 0# last[0][r]:第 r 行最近一次染黑的时间# last[1][c]:第 c 列最近一次染白的时间last = [ [-1] * (N + 1), [0] * (N + 1)]for i in range(1, Q + 1): t, x = map(int, input().split()) t -= 1 if last[t][x] == -1: # 行第一次染黑 b += N else: # 统计两次同类操作之间发生了多少次异类操作 kind = f[1 - t].sum(last[t][x], i) if t == 0: # 行染黑 b += kind else: # 列染白 b -= kind # 删除旧时间点 f[t].add(last[t][x], -1) # 记录当前操作时间 f[t].add(i, 1) last[t][x] = i print(b)涉及算法
- 时间轴思想(Last Occurrence)
- 树状数组(Fenwick Tree)
- 前缀和统计
- 在线维护答案
- 数据结构优化