登录日志 题解

登录日志 题解

一、题意

n 条日志,第 i 条为 \((t_i, b_i)\):\(t_i = 1\) 表示登录,\(t_i = 2\) 表示退出。对已在线者登录、对已离线者退出,均不改变账号状态。

\(q\) 个询问 \((l, r)\):从全离线 状态开始重放第 \(l\) 到第 \(r\) 条日志,求「执行前 + 每条执行后」共 \(r-l+2\) 个时刻中,同时在线的账号数的最大值。


二、核心思想

1. 历史最值 → 区间最值

询问要的是历史最大 在线人数,而不是「执行到 \(r\) 时」的在线人数。

例如「登录 A → 退出 A」:终态人数为 \(0\),但历史最大值为 \(1\)。

因此只固定右端点 \(r\) 维护终态会丢掉峰值 。必须固定左端点 \(l\) ,定义从全离线起始、重放第 \(l\) 到第 \(t\) 条日志之后的在线账号数:

\(f_l(t), \quad t \in l-1,\\, n,\)

并规定 \(f_l(l-1) = 0\)。于是

\\\operatorname{Ans}(l, r) = \\max_{t \\in \[l-1,\\, r} f_l(t). \]

历史最值问题,转化为时刻轴上的区间最大值问题。

对比 HH 的项链:那里种类数只增不减;本题存在退出操作,在线人数会减少,不能直接套用。

2. 定一求一(固定左端点,扫描另一端)

两个端点都在动,直接做必然超时。定住左端点 \(l\):

  1. 询问按 \(l\) 分组:\(qsl = \{(r, id), \dots\}\)。
  2. 让 \(l\) 从 \(n\) 到 \(1\) 倒序扫描 ,使线段树中始终存放整行 \(f_l(\cdot)\)。
  3. 扫到 \(l\) 时,一次性回答所有左端点为 \(l\) 的询问。

为什么定左不定右 :定右端点只能看到终态,会丢掉峰值;定左端点则整行 \(f_l(\cdot)\) 都能保留,任意时刻 \(t\) 的叶子值就是当前在线人数。

3. 在线区间并集

固定 \(l\) 后,每个账号在线的时刻构成若干区间的并集 (同一账号在同一时刻只贡献一次)。\(f_l(t)\) 表示「时刻 \(t\) 被多少个账号的在线区间并集覆盖 」,不是「并集长度」。

同一账号在位置 \(p\) 登录、下一次事件在 \(q\),则这次登录覆盖 \(p, q-1\)。无论 \(q\) 是登录还是退出,都会强制确定该账号的状态 ,因此只需用同账号日志的前驱 / 后继界定区间,nxt 不区分类型。


三、差分引理

设 nxt[i] 为账号 \(b_i\) 在位置 \(i\) 之后 第一条日志的下标(不存在时取 \(n+1\))。则

\f_l(t) - f_{l+1}(t) = \\begin{cases} 1, \& t_l = 1 \\land l \\le t \< nxt_l, \\\\ 0, \& \\text{otherwise}. \\end{cases} \\

其中 \(t_l = 1\) 表示第 \(l\) 条是登录,\(\land\) 表示「且」。

注 :起点从 \(l+1\) 左移到 \(l\),只会影响第 \(l\) 条日志所属账号这一个账号。

  • 新增登录 :该账号原本离线,\(l, nxt_l-1\) 内在线人数 +1;
  • 新增退出 :与原本离线等价,不修改;
  • 从 \(nxt_l\) 起,下一次事件让两条重放链重新一致。

要点 :nxt 的定义不区分日志类型,因此预处理时登录、退出都要更新 lst。


四、算法

在时刻轴 \(t \in 0, n\) 上建线段树,叶子存 \(f_l(t)\),初始全 0(对应空区间 \(f_{n+1} \equiv 0\))。

按 \(l = n, n-1, \dots, 1\) 倒序:

  1. 若 \(t_l = 1\):执行 change(1, l, nxt[l] - 1, +1);否则不动。
  2. 对每个 \(qsl\) 中的询问:执行 ans[id] = query(1, l, r)。

顺序必须先修改后回答 ------此时叶子才恰好代表 \(f_l\)。

预处理 nxt(倒序,lst[x] 记账号 \(x\) 最近出现的位置):


五、时间复杂度

步骤 复杂度
预处理 nxt \(O(n)\)
每条登录一次区间加 \(O(n \log n)\)
每个询问一次区间查最大 \(O(q \log n)\)
总计 \(O((n+q) \log n)\)

空间复杂度 \(O(n+q)\)。


六、AC code

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using ll = long long;

const int N = 3e5 + 5;

ll n, q;
ll t[N], b[N];

ll nxt[N], lst[N];
ll ans[N];

struct node {
	ll r, id;
};

struct NODE {
	ll l, r;
	ll maxv, tag;
};
NODE tr[N * 4];

void pushdown (int p) {
	if (tr[p].tag) {
		tr[p * 2].maxv += tr[p].tag;
		tr[p * 2 + 1].maxv += tr[p].tag;
		tr[p * 2].tag += tr[p].tag;
		tr[p * 2 + 1].tag += tr[p].tag;
		tr[p].tag = 0;
	}
}

void pushup (int p) {
	tr[p].maxv = max (tr[p * 2].maxv, tr[p * 2 + 1].maxv);
}

void build (int p, int l, int r) {
	tr[p].l = l, tr[p].r = r;
	tr[p].maxv = 0, tr[p].tag = 0;

	if (l == r) return;

	int mid = (l + r) / 2;
	build (p * 2, l, mid);
	build (p * 2 + 1, mid + 1, r);
}

void change (int p, int l, int r) {
	if (tr[p].l >= l && tr[p].r <= r) {
		tr[p].maxv += 1;
		tr[p].tag += 1;
		return;
	}

	pushdown (p);

	int mid = (tr[p].l + tr[p].r) / 2;

	if (l <= mid) change (p * 2, l, r);
	if (r > mid) change (p * 2 + 1, l, r);

	pushup (p);
}

ll query (int p, int l, int r) {
	if (tr[p].l >= l && tr[p].r <= r) return tr[p].maxv;

	pushdown (p);

	int mid = (tr[p].l + tr[p].r) / 2;
	ll res = 0;

	if (l <= mid) res = max (res, query (p * 2, l, r) );
	if (r > mid) res = max (res, query (p * 2 + 1, l, r) );

	return res;
}

int main () {
	freopen("login.in", "r", stdin);
	freopen("login.out", "w", stdout);

	ios::sync_with_stdio (false);
	cin.tie (0);

	cin >> n >> q;

	for (int i = 1; i <= n; i ++) {
		cin >> t[i] >> b[i];
	}

	vector<vector<node> > que (n + 1);

	for (int i = 1; i <= q; i ++) {
		ll l, r;
		cin >> l >> r;

		que[l].push_back ({ r, i });
	}

	for (int i = n; i >= 1; i --) {
		if (!lst[b[i]]) nxt[i] = n + 1;
		else nxt[i] = lst[b[i]];

		lst[b[i]] = i;
	}

	build(1, 1, n);
	
	for (int l = n; l >= 1; l --) {
		if (t[l] == 1) change (1, l, nxt[l] - 1);

		for (auto &[r, id] : que[l]) {
			ans[id] = query (1, l, r);
		}
	}

	for (int i = 1; i <= q; i ++) cout << ans[i] << '\n';

	return 0;
}


总结

  • 历史最值转区间最值;

  • 定一求一按左端点扫描;

  • 在线区间并集

相关推荐
CQU_JIAKE1 小时前
10.2【A】
数据结构
C++ 老炮儿的技术栈2 小时前
不一样的数值交换
数据结构·c++·人工智能·算法·c·csdn开发云
青山木3 小时前
Hot 100 --- 编辑距离
java·数据结构·算法·leetcode·动态规划
梦帮科技4 小时前
vLLM / TensorRT-LLM 极限推理:PagedAttention 细粒度物理页表管理与连续批处理(Continuous Batching)实战
数据结构·人工智能·分布式·python·深度学习·算法·vllm
H.莓飛6 小时前
【数据结构】链表_OJ题
linux·数据结构·算法·链表
All for pursuit.6 小时前
【回溯-4】46.全排列
数据结构·c++·算法·leetcode
7yewh9 小时前
SLAM 从视觉里程计到建图(6)
数据结构·人工智能·机器人·自动驾驶·嵌入式
程序员清风9 小时前
pandas 核心数据结构:Series 与 DataFrame
数据结构·pandas
All for pursuit.10 小时前
【回溯-5】78.子集
数据结构·c++·算法·leetcode