关于我想了好久才想出这题咋做这档事 - 8

#Prob. 1 CF1354D Multise

Time Limit: 1.5s | Memory Limit: 28MB

#题意简述

维护一个可重集,最初有 \(n(n\leq10^6)\) 个元素 \(a_i(a_i\in[1,n])\),有 \(q(q\leq10^6)\) 次操作,分为以下两种:

  • \(k<0\),删除排名为 \(|k|\) 的数字,若不存在,则忽略;
  • \(k>0\),加入一个 \(k\),满足 \(k\in[1,n]\)

最后输出可重集中的任意一个数字即可。

#大体思路

空间限制比较紧,平衡树很有可能会被卡空间,这里考虑用权值线段树动态开点实现,插入时直接单点加一即可,删除时类似于平衡树在线段树上二分即可(线段树本身就是二分结构),维护各个节点的区间和即可。时间、空间复杂度为 \(O(n\log n)\).

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const int N = 4000010;
const int INF = 0x3fffffff;

int n, q, cnt, rt = 1, p[N];

void add(int k, int l, int r, int x) {
++ p[k]; if (l == r) return;
int mid = l + r >> 1;
if (x <= mid) add(k << 1, l, mid, x);
else add(k << 1 | 1, mid + 1, r, x);
}

int del(int k, int l, int r, int x) {
-- p[k]; if (l == r) return l;
int mid = l + r >> 1, lsum = p[k << 1];
if (lsum >= x) return del(k << 1, l, mid, x);
else return del(k << 1 | 1, mid + 1, r, x - lsum);
}

int main() {
scanf("%d%d", &n, &q); int k;
for (int i = 1; i <= n; ++ i)
scanf("%d", &k), add(rt, 1, n, k);
while (q --) {
scanf("%d", &k);
if (k >= 0) add(rt, 1, n, k);
else if (p[rt] >= -k) del(rt, 1, n, -k);
}
if (p[rt] <= 0) printf("0");
else printf("%d", del(rt, 1, n, 1));
return 0;
}

#Prob. 2 CF1181D Irrigation

Time Limit: 2.5s | Memory Limit: 512MB

#题意简述

给定 \(m(m\leq5\cdot10^5)\) 个城市,每年会选出一个城市举办比赛,现给出前 \(n(n\leq5\cdot10^5)\) 年城市举办比赛的情况。在接下来的年份中,每年的比赛会在举办比赛次数最小的城市举办,如果有很多城市举办次数均为最小值,则在编号最小的城市举办比赛。现给出 \(q(q\leq5\cdot10^5)\) 个询问,每次询问第 \(K(n+1\leq K\leq10^{18})\) 年在哪个城市举办比赛。

#大体思路

注意到我们一定是先选累计举办次数少的城市举办比赛,于是我们考虑将最初的城市按照给出的累计比赛次数升序排序,然后将询问离线。

根据题面,一个城市 \(x\) 被选的前提是最初选择次数比 \(x\) 的次数少的城市,当前累计选择次数与 \(x\) 相同,用此时前面所有的城市的累计选择次数的和减去初始的所有累计次数,我们称得到的值为 \(x\)解锁值,如图(图咕咕咕了)

这里应当有一张图,但它咕咕咕了

我们将上面的语言用图形化的形式表示出来,也就是 \(x\) 前面的矩形的高度与其持平时才可以将其解锁,显然每个 \(x\) 的解锁值可以通过计算矩形面积的方式进行 \(O(n)\) 递推。

于是我们将所有询问排序,对于每一个询问,考虑将这个询问之前能解锁的所有城市加入候选序列,然后发现在解锁下一个城市之前,已解锁区间一定是以一个按编号循环的形式进行选择,也就是当前询问次数减去解锁值后,模当前已解锁序列中的城市个数 \(tot\) 得到的数(如果是 \(0\) 那么就是 \(tot\)),对应的就是应选城市的排名,用权值线段树维护即可。

#时间复杂度分析

  • 排序为 \(O(n\log n+m\log m)\)
  • 求解锁值为 \(O(n)\)
  • 每个城市最多加入序列一次,总体为 \(O(n\log n)\),每个询问最多查询一次,总体时间复杂度为 \(O(m\log n)\)
  • 综上所述,由于 \(n,m\) 同阶,于是总体时间复杂度为 \(O(n\log n)\).

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#define ll long long
#define int long long

const int M = 500010;
const int N = 2000010;
const int INF = 0x3fffffff;

struct City {
int val, idx;
inline bool operator < (const City b) const {
return val < b.val;
}
} a[N];

struct Query {
ll val; int idx;
inline bool operator < (const Query b) const {
return val < b.val;
}
} q[N];

int n, m, t, cnt, ans[M], sum[N], rt = 1, p = 1;
ll lmt[M], lim;

void insert(int k, int l, int r, int x) {
if (l == r) {++ sum[k]; return;}
int mid = l + r >> 1;
if (x <= mid) insert(k << 1, l, mid, x);
else insert(k << 1 | 1, mid + 1, r, x);
sum[k] = sum[k << 1] + sum[k << 1 | 1];
}

int kth(int k, int l, int r, int x) {
if (l == r) {return l;}
int mid = l + r >> 1, lsum = sum[k << 1];
if (x <= lsum) return kth(k << 1, l, mid, x);
else return kth(k << 1 | 1, mid + 1, r, x - lsum);
}

signed main() {
scanf("%d%d%d", &n, &m, &t);
for (int i = 1; i <= n; ++ i) {
int x; scanf("%d", &x); ++ a[x].val;
}
for (int i = 1; i <= m; ++ i) a[i].idx = i;
for (int i = 1; i <= t; ++ i) {
scanf("%lld", &q[i].val);
q[i].val -= (ll)n, q[i].idx = i;
}
sort(a + 1, a + m + 1); sort(q + 1, q + t + 1);
lim = a[m].val * m - n;
for (int i = 1; i <= m; ++ i)
lmt[i] = lmt[i - 1] + ((i - 1) * (a[i].val - a[i - 1].val));
insert(rt, 1, m, a[p ++].idx);
for (int i = 1; i <= t; ++ i) {
if (q[i].val > lim) {
if ((q[i].val - lim) % m)
ans[q[i].idx] = (q[i].val - lim) % m;
else ans[q[i].idx] = m;
continue;
}
while (p <= m && q[i].val > lmt[p])
insert(rt, 1, m, a[p ++].idx);
int rk = q[i].val - lmt[p - 1];
if (rk % (p - 1) == 0) rk = p - 1;
else rk = rk % (p - 1);
ans[q[i].idx] = kth(rt, 1, m, rk);
}
for (int i = 1; i <= t; ++ i) printf("%d\n", ans[i]);
return 0;
}

#Prob. 3 [NOI2004]郁闷的出纳员

Time Limit: 1s | Memory Limit: 128MB

#题意简述

经典老题了属于是。

给定一个下界 \(0\leq m\leq10^9\),有 \(n(n\leq3\cdot10^5)\) 个操作,分为 \(4\) 种:

  • I k 向数列中加入一个大小为 \(k\) 的数,如果 \(k<m\),则忽略;
  • A k 把数列中所有的数加上 \(k\)
  • S k 把数列中的所有数减去 \(k\),如果数列中有数小于 \(m\),那么将其从数列中删除;
  • F k 查询第 \(k\) 大的工资,没有则为 -1

最后输出一共删除了多少数(未加入不算)。

#大体思路

权值线段树的水题,当然用平衡树也可以做。

对于所有的数加/减的操作,我们直接维护一个全局变量 \(\Delta\),表示真实值与当前权值线段树中的值的差值;

对于全局减后的删除操作,我们在权值线段树上二分,如果整个左子树都小于 \(m-\Delta\),那就整个删掉左子树(采用动态开点,直接令 lson = 0),然后递归右子树,否则递归左子树,显然这样的递归最多 \(O(\log n)\) 层,于是单次删除的时间复杂度为 \(O(\log n)\).

单点插入 \(k\) 直接经典插入即可,不过要注意 \(\Delta\) 对于 \(k\) 也是起作用的,但是它不应该起作用,所以我们需要插入的是 \(k-\Delta\) 来抵消 \(\Delta\) 的错误作用。

查询时直接在权值线段树上二分即可。

总体时间复杂度为 \(O(n\log n)\),上面的 \(\Delta\) 显然可以转变为直接对 \(m\) 操作,两种方式本质相同。

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const int N = 2000010;
const int UPL = 300010;
const int INF = 0x3fffffff;

struct Node {int ls, rs, sum;} p[N];

int n, m, cnt, ans, tag, rt;

inline void pushup(int k) {
p[k].sum = p[p[k].ls].sum + p[p[k].rs].sum;
}

inline void add(int &k, int l, int r, int x) {
if (!k) k = ++ cnt;
if (l == r) {++ p[k].sum; return;}
int mid = l + r >> 1;
if (x <= mid) add(p[k].ls, l, mid, x);
else add(p[k].rs, mid + 1, r, x);
pushup(k);
}

inline void update(int &k, int l, int r, int x) {
if (!k) return;
int mid = l + r >> 1;
if (mid > x) update(p[k].ls, l, mid, x);
else {
p[k].sum -= p[p[k].ls].sum;
ans += p[p[k].ls].sum; p[k].ls = 0;
} if (mid < x) update(p[k].rs, mid + 1, r, x);
pushup(k);
}

inline int kth(int k, int l, int r, int x) {
if (l == r) return l;
int mid = l + r >> 1, rsum = p[p[k].rs].sum;
if (rsum >= x) return kth(p[k].rs, mid + 1, r, x);
else return kth(p[k].ls, l, mid, x - rsum);
}

int main() {
scanf("%d%d", &n, &m);
char c; int x;
while (n --) {
cin >> c >> x;
if (c == 'I') {
if (x < m) continue;
add(rt, -UPL, UPL, x - tag);
} else if (c == 'A') tag += x;
else if (c == 'S') {
tag -= x;
update(rt, -UPL, UPL, m - tag - 1);
} else {
if (x > p[rt].sum) {printf("-1\n"); continue;}
printf("%d\n", kth(rt, -UPL, UPL, x) + tag);
}
}
printf("%d", ans);
return 0;
}

#Prob. 4 CF600E Lomsat gelral

Time Limit: 2s | Memory Limit: 256MB

#题意简述

有一棵 \(n\) 个结点的以 \(1\) 号结点为根的有根树

每个结点都有一个颜色,颜色是以编号表示的, \(i\) 号结点的颜色编号为 \(c_i\).

如果一种颜色在以 \(x\) 为根的子树内出现次数最多,称其在以 \(x\) 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。

你的任务是对于每一个 \(i\in[1,n]\),求出以 \(i\) 为根的子树中,占主导地位的颜色的编号和。

\(n\le 10^5,c_i\le n\).

#大体思路

线段树合并水题,树上的每个节点都有一棵线段树,直接在树上自底向上合并即可。

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#define ll long long

const int N = 2000010;
const int INF = 0x3fffffff;

struct Node {int ls, rs; ll val, tot;} p[N];
struct Edge {int u, v, nxt;} e[N];

int n, c[N], rt[N], head[N], ecnt = 1;
int ref[N], rcnt, cnt; ll f[N];


inline int New() {return rcnt ? ref[rcnt --] : ++ cnt;}

inline void recover(int x) {
p[x].ls = p[x].rs = p[x].tot = p[x].val = 0;
ref[++ rcnt] = x;
}

inline void pushup(int k) {
if (p[p[k].ls].val > p[p[k].rs].val)
p[k].val = p[p[k].ls].val, p[k].tot = p[p[k].ls].tot;
else if (p[p[k].ls].val < p[p[k].rs].val)
p[k].val = p[p[k].rs].val, p[k].tot = p[p[k].rs].tot;
else p[k].val = p[p[k].ls].val, p[k].tot = p[p[k].ls].tot + p[p[k].rs].tot;
}

inline void add(int u, int v) {
e[cnt].u = u, e[cnt].v = v;
e[cnt].nxt = head[u], head[u] = cnt ++;
}

void update(int &k, int l, int r, int x) {
if (!k) k = New();
if (l == r) {p[k].tot = (ll)l, ++ p[k].val; return;}
int mid = (l + r) >> 1;
if (x <= mid) update(p[k].ls, l, mid, x);
else update(p[k].rs, mid + 1, r, x);
pushup(k);
}

int merge(int k1, int k2, int l, int r) {
if (!k1 || !k2) return k1 + k2;
if (l == r) {
p[k1].val += p[k2].val;
recover(k2); return k1;
}
int mid = (l + r) >> 1;
p[k1].ls = merge(p[k1].ls, p[k2].ls, l, mid);
p[k1].rs = merge(p[k1].rs, p[k2].rs, mid + 1, r);
pushup(k1); recover(k2); return k1;
}

void dfs(int x, int fa) {
for (int i = head[x]; i; i = e[i].nxt) {
if (e[i].v == fa) continue;
dfs(e[i].v, x);
rt[x] = merge(rt[x], rt[e[i].v], 1, n);
}
f[x] = p[rt[x]].tot;
}

int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++ i) {
scanf("%d", &c[i]);
update(rt[i], 1, n, c[i]);
}
for (int i = 1; i < n; ++ i) {
int u, v; scanf("%d%d", &u, &v);
add(u, v); add(v, u);
}
dfs(1, 0);
for (int i = 1; i <= n; ++ i)
printf("%lld ", f[i]);
return 0;
}

#Prob. 5 [POI2011]ROT-Tree Rotations

Time Limit: 1s | Memory Limit: 128MB

#题意简述

给定一颗有 \(n\)叶节点的二叉树。每个叶节点都有一个权值 \(p_i\)(注意,根不是叶节点),所有叶节点的权值构成了一个 \(1 \sim n\) 的排列。 对于这棵二叉树的任何一个结点,保证其要么是叶节点,要么左右两个孩子都存在。 现在你可以任选一些节点,交换这些节点的左右子树。 在最终的树上,按照先序遍历遍历整棵树并依次写下遇到的叶结点的权值构成一个长度为 \(n\) 的排列,你需要最小化这个排列的逆序对数。

#大体思路

首先不难看出,一个子树内的逆序对与外部是毫不相干的,于是考虑自底向上的合并,对于逆序对这个东西,很套路地考虑分治进行统计,对于当前区间,考虑中点左右分别在左右两棵子树的情况,然后对于交换与不交换分别统计答案,然后再分治到中点左右两个小区间,这一步可以在合并左右子树时一并完成。然后对于每个父节点,选择代价较小者即可。

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#define ll long long
#define int long long

const int N = 5000010;
const int INF = 0x3fffffff;

template <typename T>
inline T Min(const T a, const T b) {return a < b ? a : b;}

struct Node {int sum, ls, rs;} p[N];

int n, cnt, rt;
ll lsum, rsum, ans;

inline void pushup(int k) {
p[k].sum = p[p[k].ls].sum + p[p[k].rs].sum;
}

void build(int &k, int l, int r, int x) {
if (!k) k = ++ cnt;
if (l == r) {p[k].sum += 1; return;}
int mid = (l + r) >> 1;
if (x <= mid) build(p[k].ls, l, mid, x);
else build(p[k].rs, mid + 1, r, x);
pushup(k);
}

int merge(int k1, int k2, int l, int r) {
if (!k1 || !k2) return k1 + k2;
if (l == r) {p[k1].sum += p[k2].sum; return k1;}
lsum += (ll)(p[p[k1].rs].sum * p[p[k2].ls].sum);
rsum += (ll)(p[p[k2].rs].sum * p[p[k1].ls].sum);
int mid = (l + r) >> 1;
p[k1].ls = merge(p[k1].ls, p[k2].ls, l, mid);
p[k1].rs = merge(p[k1].rs, p[k2].rs, mid + 1, r);
pushup(k1); return k1;
}

int dfs() {
int p = 0, op; scanf("%lld", &op);
if (!op) {
int ls = dfs(), rs = dfs();
lsum = rsum = 0;
p = merge(ls, rs, 1, n);
ans += Min(lsum, rsum);
} else build(p, 1, n, op);
return p;
}

signed main() {
scanf("%lld", &n); rt = dfs();
printf("%lld", ans); return 0;
}

#Prob. 6 HDU6606 Distribution of books

Time Limit: 8s | Memory Limit: 64MiB

#Prob. 7 HDU6609 Find the answer

Time Limit: 4s | Memory Limit: 64MiB

#题意简述

给定一个长度为 \(n(n\leq2\cdot10^5)\) 的序列 \(a_i(a_i\leq10^9)\) 和上界 \(m(m\leq10^9)\),要求对于每个 \(i\) 求出至少使多少个 \(a_k(1\leq k<i)\) 变为 \(0\) 才能使得 \(\sum_{j=1}^ia_j\leq m\).

#大体思路

将原题简单转换一下为:选出尽可能多的数使得和不超过 \(m\),那么一定是尽可能选小的,于是我们直接用权值线段树维护,查询时直接在权值线段树上二分即可。

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#define ll long long

const int N = 5000010;
const int INF = 0x3fffffff;

struct Node {int ls, rs, tot; ll sum;} p[N];

int t, n, cnt, rt; ll a[N], m;

inline void clear() {
for (int i = 1; i <= cnt; ++ i)
p[i].sum = p[i].tot = p[i].ls = p[i].rs = 0;
cnt = rt = 0;
}

inline void pushup(int k) {
p[k].sum = p[p[k].ls].sum + p[p[k].rs].sum;
p[k].tot = p[p[k].ls].tot + p[p[k].rs].tot;
}

void update(int &k, int l, int r, ll x) {
if (!k) k = ++ cnt;
if (l == r) {p[k].sum += x, ++ p[k].tot; return;}
int mid = (l + r) >> 1;
if (x <= mid) update(p[k].ls, l, mid, x);
else update(p[k].rs, mid + 1, r, x);
pushup(k);
}

int query(int k, int l, int r, ll s) {
if (!k) return 0;
if (l == r) {
if (l == 0) return 0;
int res = s / l;
if (res <= p[k].tot) return res;
else return p[k].tot;
}
int mid = (l + r) >> 1; ll lsum = p[p[k].ls].sum;
if (lsum >= s) return query(p[k].ls, l, mid, s);
else return p[p[k].ls].tot + query(p[k].rs, mid + 1, r, s - lsum);
}

int main() {
scanf("%d", &t);
while (t --) {
clear();
scanf("%d%lld", &n, &m);
for (int i = 1; i <= n; ++ i) scanf("%lld", &a[i]);
for (int i = 1; i <= n; ++ i) {
printf("%d ", i - 1 - query(rt, 0, m, m - a[i]));
update(rt, 0, m, a[i]);
}
printf("\n");
}
return 0;
}