2019 年电子科技大学 ACM 暑期前集训数据结构专题解题报告

https://acm.uestc.edu.cn/problem/fang-chai

(请先看 n题) 这题也是一道线段树的题目,题目中的方差可以拆成和、平方和两个数据来维护,这样合并就很方便。而数据变化有加、乘、抹平两种操作。根据乘法的分配率等定理,多次加乘最后可以化简为一次乘和一次加,为避免出现分数,整合为先乘后加比较方便。而抹平操作则可理解为乘 0再加。这样则有两个标记。这题与 n题相比最关键的区别在于,该题先乘后加的二项式操作不具有结合律,须在更新下层元素之前 pushdown。

  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
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int mod = 1000000007;
inline ll M(ll n) { return (n + mod) % mod; }
int num[100000];
ll ans_sum, ans_s2;
struct tnode {//对节点需要赋初值、更新值、清除标记合并等操作,其中平方和的更新比 n题稍复杂,还有乘标记的默认值为 1,标记的合并也值得注意。
  int l, r;
  ll sum, s2, mark1 = 1, mark2 = 0;
  inline tnode() {}
  inline tnode(int l, int r) {
    this->l = l, this->r = r;
    if (l == r) {
      this->sum = num[l - 1];
      this->s2 = M(this->sum * this->sum);
    }
  }
  void change(int multipie, int add) {
    this->s2 = M(M(this->s2 * M((ll)multipie * multipie)) +
                 2 * M(M((ll)add * multipie) * this->sum) +
                 M(M((ll)add * add) * this->len()));
    this->sum = M(this->sum * multipie + (ll)add * this->len());
    this->mark1 = M(this->mark1 * multipie);
    this->mark2 = M(this->mark2 * multipie + add);
  }
  inline void markdel() {
    this->mark1 = 1;
    this->mark2 = 0;
  }
  inline int mid() { return (this->l + this->r) >> 1; }
  inline void merge(tnode lc, tnode rc) {
    this->sum = M(lc.sum + rc.sum);
    this->s2 = M(lc.s2 + rc.s2);
  }
  inline int len() { return r - l + 1; }
};

tnode node[400000];
void buildtree(int l, int r, int pos = 1) {//由于特殊点都写进了成员函数,后面按标准的线段树写就好
  tnode &n = node[pos];
  n = tnode(l, r);
  if (l == r) return;
  int lc = pos << 1, rc = pos << 1 | 1;
  buildtree(l, n.mid(), lc);
  buildtree(n.mid() + 1, r, rc);
  n.merge(node[lc], node[rc]);
}

inline void pushdown(int pos) {
  tnode &n = node[pos];
  node[pos << 1].change(n.mark1, n.mark2);
  node[pos << 1 | 1].change(n.mark1, n.mark2);
  n.markdel();
}

void query(int l, int r, int pos = 1) {
  tnode &n = node[pos];
  if (l > n.r || r < n.l) return;
  if (l <= n.l && r >= n.r) {
    ans_sum = M(ans_sum + n.sum);
    ans_s2 = M(ans_s2 + n.s2);
  } else {
    if (n.mark1 != 1 || n.mark2 != 0) pushdown(pos);
    query(l, r, pos << 1);
    query(l, r, pos << 1 | 1);
  }
}

void update(int op, int l, int r, int k, int pos = 1) {
  tnode &n = node[pos];
  if (l > n.r || r < n.l) return;
  if (l <= n.l && r >= n.r) switch (op) {//三种操作,加可以认为是先乘 1,乘可以认为加 0,抹平则是先乘 0
      case 1:
        n.change(1, k);
        break;
      case 2:
        n.change(k, 0);
        break;
      case 3:
        n.change(0, k);
    }
  else {
    int lc = pos << 1, rc = pos << 1 | 1;
    pushdown(pos);//更新子节点前先处理标记
    update(op, l, r, k, lc);
    update(op, l, r, k, rc);
    n.merge(node[lc], node[rc]);
  }
}
int main() {
  int n, q;
  scanf("%d%d", &n, &q);
  for (int i = 0; i < n; ++i) scanf("%d", num + i);
  buildtree(1, n);
  while (q--) {
    int op;
    int l, r;
    scanf("%d%d%d", &op, &l, &r);
    if (op == 4) {
      ans_sum = ans_s2 = 0;
      query(l, r);
      printf("%lld\n", M(ans_s2 * (r - l + 1) - M(ans_sum * ans_sum)));
    } else {
      int k;
      scanf("%d", &k);
      update(op, l, r, k);
    }
  }
}

https://acm.uestc.edu.cn/problem/tun-tu-liang

这是一道典型的 LCA,另外建树部分使用邻接表(邻接矩阵内存爆炸,边读边建树想了很久也没想到可行方法)。查阅一些资料后,本来对 Tarjan 比较有想法,但是写代码的时候出了一些问题,后来就去写倍增了。

 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
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct edge {
  int num, l;
};
list<edge> path[100001];
int dep[100001], U[100001][18], L[100001][18];
void buildtree(int pos = 1) {//根节点为 1,从根开始对每个节点遍历相邻的边,遇到未加入树的就添加,并且递归操作。遍历结束后 clear()释放无用内存
  for (edge p : path[pos])
    if (!U[p.num][0]) {
      U[p.num][0] = pos;
      dep[p.num] = dep[pos] + 1;
      L[p.num][0] = p.l;
      buildtree(p.num);
    }
  path[pos].clear();
}
int find(int u, int v) {//核心思路就是类似于二分的方法,通过指数的变化逼近取值,将复杂度降到 O(logn)
  if (u == v) return 0;
  int ans = INT_MAX;
  if (dep[u] < dep[v]) swap(u, v);
  while (dep[u] - dep[v] > 1) {//把两者的深度调至相同,由于有已知的深度差,通过对数计算确定跳的步数,理论上效率应该比像后面那样从最大值开始循环略高
    int lo = log(dep[u] - dep[v]) / log(2);
    ans = min(ans, L[u][lo]), u = U[u][lo];
  }
  if (dep[u] != dep[v]) ans = min(ans, L[u][0]), u = U[u][0];
  if (u == v) return ans;
  for (int i = 17; i >= 0; --i) {//目标深度未知,只能循环判断了
    if (i && U[u][i] == U[v][i]) {
      continue;
    }
    ans = min(ans, min(L[u][i], L[v][i]));
    u = U[u][i], v = U[v][i];
  }
  if (u == v) return ans;
  return min(ans, min(L[u][0], L[v][0]));
}
int main() {
  int n, q;
  scanf("%d%d", &n, &q);
  for (int i = 1; i < n; ++i) {
    int u, v, w;
    scanf("%d%d%d", &u, &v, &w);
    path[u].push_back({v, w});
    path[v].push_back({u, w});
  }
  U[1][0] = 1;//建树前用邻接表添加边,并把根节点 1加入树,
  buildtree();
  for (int i = 1; 1 << i < n; ++i)//递推预处理
    for (int j = 1; j <= n; ++j)
      U[j][i] = U[U[j][i - 1]]
[i - 1],
      L[j][i] = min(L[j][i - 1], L[U[j][i - 1]]
[i - 1]);
  while (q--) {
    int u, v;
    scanf("%d%d", &u, &v);
    printf("%d\n", find(u, v));
  }
}

https://acm.uestc.edu.cn/problem/ren-zai-di-shang-zou-guo-cong-tian-shang-lai

这道题处理一条线段上面联通块的个数,并且是动态的。我们可以把各个联通块按顺序排列起来,这样查找更新都比较方便,而 set 容器很好地提供了排列和去重的功能,优先队列虽然效率不错但不提供删除功能。

 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
#include <bits/stdc++.h>
using namespace std;
struct guo {//每个黑锅的左右端点作为一个节点
  int l, r;
  guo(int l, int r) { this->l = l, this->r = r; }
  bool operator<(guo ans) const { return this->r < ans.l; }//不加 const 过不了编译,重载运算符。当 a<b、b<a 均为假时,系统会认为 a==b,因此只需重载一个比较运算符。
};
inline guo merge(guo a, guo b) { return guo(min(a.l, b.l), max(a.r, b.r)); }//合并两个黑锅
int main() {
  int n;
  scanf("%d", &n);
  set<guo> a;
  while (n--) {
    int l, r;
    scanf("%d%d", &l, &r);
    guo hei = guo(l, r);
    auto it = a.find(hei);
    while (it != a.end()) {//找到所有和新的黑锅联通的黑锅,合并,删除
      hei = merge(hei, *it);
      a.erase(it);
      it = a.find(hei);
    }
    a.insert(hei);
    printf("%lu", a.size());
    if (n) putchar(' ');
  }
}

https://acm.uestc.edu.cn/problem/mi-ma-1

这是我做的最憋屈的一题,因为k==0的情况WA了十多次。。。好在最后善良的出题人在note里给了提示。

首先这题中要求输出各元素的相对位置不变,这符合队列的性质,而同时又要求代表的十六进制值最大,所以使用单调队列来解决这个问题。我在找到WA的原因之前,想了很久,改了不少次,折腾了很久。好像也可以线段树,就是会多个log,也应该不会卡。

 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
#include <deque>
#include <iostream>
using namespace std;
int main() {
  ios::sync_with_stdio(false);
  int n, k;
  while (cin >> n >> k) {
    deque<char> nums;
    char tmp;
    if (!k) {//k 为0 时直接读完输入进入下一组数据,最后 AC 的关键,其实 getline 可能更好,但是影响很小,所以不重交了
      while (n--) cin >> tmp;
      //这里有无 cout<<endl;均可
      continue;
    }
    cin >> tmp;
    nums.push_front(tmp);
    for (int i = 0; i < n - k; ++i) {//前半部分直接按单调队列的规则来
      cin >> tmp;
      while (!nums.empty() && nums.front() < tmp) nums.pop_front();
      nums.push_front(tmp);
    }
    for (int i = 1; i < k; ++i) {//后半部分一边读取新元素,一边输出并删除最优元素,可确保个数足够。把两个循环拆开,常数应该比循环内加判断小 1(其实是因为我之前写错误代码的时候拆了,然后就不想改了)
      cout << nums.back();
      cin >> tmp;
      nums.pop_back();
      while (!nums.empty() && nums.front() < tmp) nums.pop_front();
    };
    cout << nums.back() << endl;//第二个循环少输出了一个字符,现在补上,导致 k为 0时也会输出一个字符,从而 WA
  }
}

https://acm.uestc.edu.cn/problem/dong-ma-he-sha-tian-xia-di-yi/description

首先,任意个不同的数都可以构成一个单调序列,因此,这题实际上是要求区间中出现最多的数出现的次数,即区间众数。区间众数是一个很经典的难题,常见的做法有分块、莫队(离线)等等。这道题要用上一个询问的答案来处理下一次询问,因此离线做法不可行。

不管是那种做法,我们首先都可以把数列离散化一下,然后考虑分块做法,把数列分为$\sqrt{n}$大小的块,然后我们可以求一个前缀和,即从第一块到每一块的每个元素的总出现次数,对每个块都暴力遍历一遍即可,$O(n\sqrt{n})$。然后再以块为单位枚举起点和终点,从第i块到第j块的区间众数要么是从第i块到第j-1块的众数,要么就是出现在第j块中的数,这样利用前缀和可以在$O(\sqrt{n})$内求出每两个块的值,这样复杂度为$O((\sqrt{n})^2 \sqrt{n})$预处理就结束了,然后查询就很方便,我们可以直接得到区间中整块部分的众数,再在两端的不完整的块中一个一个地暴力统计即可,复杂度也是$O(n\sqrt{n})$。

还有一些别的写法,比如预处理各块自己的众数,然后用n个vector存各点的出现位置,然后用lowerbound差分找答案,这种写法常数非常小,对于随机数据比较快,但是复杂度多了一个$O(log(众数出现次数))$,数据大量重复时就会很慢。

参考代码:

 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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
  #include <bits/stdc++.h>
  using namespace std;
  using ll = long long;
  #define RST(a) memset(a, 0, sizeof(a))
  #define RSTV(a, v) memset(a, v, sizeof(a))
  #define FOR(i, a, b) for (auto i = (a); i < (b); ++i)
  const int INF = 0x3f3f3f3f;
  int n, m, Size;
  int a[40001], pos[40001];
  int sum[201][40001];
  int res[201][201];
  int S[201];
  void init() {
    int bs = n / Size;
    FOR(i, 0, bs) {
      S[i] = Size;
      FOR(j, 0, Size * (i + 1))
      ++sum[i][a[j]];
    }
    if (n % Size) {
      S[bs] = n % Size;
      FOR(j, 0, n)
      ++sum[bs][a[j]];
    }
    FOR(i, 0, S[0])
    res[0][0] = max(res[0][0], sum[0][a[i]]);
    FOR(rblock, 1, bs) {
      res[0][rblock] = res[0][rblock - 1];
      FOR(i, rblock * Size, rblock * Size + S[rblock])
      res[0][rblock] = max(res[0][rblock], sum[rblock][a[i]]);
    }
    FOR(lblock, 1, bs) FOR(rblock, lblock, bs) {
      res[lblock][rblock] = res[lblock][rblock - 1];
      FOR(i, rblock * Size, rblock * Size + S[rblock])
      res[lblock][rblock] =
          max(res[lblock][rblock], sum[rblock][a[i]] - sum[lblock - 1][a[i]]);
    }
  }
  int in_int() {
    char c = getchar();
    int ans = 0;
    while (c > '9' || c < '0') c = getchar();
    while (c >= '0' && c <= '9') {
      ans = (ans << 3) + (ans << 1) + c - '0';
      c = getchar();
    }
    return ans;
  }
  int cnt[40001];
  int query(int l, int r) {
    RST(cnt);
    int maxc = 0;
    int lblock = l / Size, rblock = r / Size;
    if (lblock == rblock) FOR(i, l, r + 1) {
        ++cnt[a[i]];
        maxc = max(maxc, cnt[a[i]]);
      }
    else {
      // if (l % Size == 0) --lblock;
      // if ((r + 1) % Size == 0) ++rblock;
      FOR(i, l, Size * (lblock + 1)) {
        if (!cnt[a[i]]) cnt[a[i]] = sum[rblock - 1][a[i]] - sum[lblock][a[i]];
        ++cnt[a[i]];
        maxc = max(maxc, cnt[a[i]]);
      }
      FOR(i, Size * rblock, r + 1) {
        if (!cnt[a[i]]) cnt[a[i]] = sum[rblock - 1][a[i]] - sum[lblock][a[i]];
        ++cnt[a[i]];
        maxc = max(maxc, cnt[a[i]]);
      }
      if (lblock + 1 < rblock) maxc = max(maxc, res[lblock + 1][rblock - 1]);
    }
    return maxc;
  }
  int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    n = in_int();
    m = in_int();
    Size = sqrt(n);
    FOR(i, 0, n) a[i] = in_int();
    memcpy(pos, a, sizeof(int) * n);
    sort(pos, pos + n);
    auto lst = unique(pos, pos + n);
    FOR(i, 0, n) a[i] = lower_bound(pos, lst, a[i]) - pos;
    init();
    int result = 0;
    FOR(i, 0, m) {
      int l = in_int(), r = in_int();
      l = (l + result - 1) % n;
      r = (r + result - 1) % n;
      if (l > r) swap(l, r);
      printf("%d\n", result = query(l, r));
    }
  }

https://acm.uestc.edu.cn/problem/wo-yong-yuan-xi-huan-dong-ma-he-sha/description

这题在上题的基础上,可以使用离线算法,但是对复杂度、常数的要求都较高。上题的思路无法满足本题的要求,本题可以使用莫队算法来解决问题。我们已知一个区间的状态(各数出现的次数,众数出现的次数,以及出现次数为任意值的元素各有多少个),可以很快地得出其一个端点加一(或减一)后的状态,根据增加或减少的数维护上面三个变量都是$O(1)$的。然后我们就要考虑一个顺序,依次得到各个询问的区间结果,而且复杂度较低。如果简单地以主次关键字排序,次要的端点可能会反复来回长距离移动,复杂度最坏可到$O(n^2)$。这里我们可以对一个端点进行分块,同一块内的询问按另一个端点排序,这样该端点只会在$\sqrt{n}$范围内来回移动(跨块移动是单调的),另一个端点只会来回移动$\sqrt{n}$次,这样即可保证复杂度为$O(n\sqrt{n})$。

参考代码:

 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
78
79
80
81
82
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = (a); i < (b); ++i)
const int INF = 0x3f3f3f3f;

int n, m, Size;
int a[200001], pos[200001];
int in_int() {
  char c = getchar();
  int ans = 0;
  while (c > '9' || c < '0') c = getchar();
  while (c >= '0' && c <= '9') {
    ans = (ans << 3) + (ans << 1) + c - '0';
    c = getchar();
  }
  return ans;
}

int pp = 0, result[200001];
struct Q {
  int l, r, p, b;
  void get() {
    l = in_int() - 1;
    r = in_int() - 1;
    p = pp++;
    b = l / Size;
  }
  bool operator<(const Q& x) {
    return b ^ x.b ? b < x.b : b & 1 ? r > x.r : r < x.r;
  }
} query[200001];
int cnt[200001], sum[200001], res = 0;
inline void addr(int& R) {
  --sum[cnt[a[++R]]++];
  ++sum[cnt[a[R]]];
  res = max(res, cnt[a[R]]);
}
inline void eraser(int& R) {
  --sum[cnt[a[R]]];
  if (cnt[a[R]] == res && !sum[cnt[a[R]]]) --res;
  ++sum[--cnt[a[R--]]];
}
inline void addl(int& L) {
  --sum[cnt[a[--L]]++];
  ++sum[cnt[a[L]]];
  res = max(res, cnt[a[L]]);
}
inline void erasel(int& L) {
  --sum[cnt[a[L]]];
  if (cnt[a[L]] == res && !sum[cnt[a[L]]]) --res;
  ++sum[--cnt[a[L++]]];
}
void solve() {
  int L = 0, R = -1;
  FOR(i, 0, m) {
    auto& q = query[i];
    while (R < q.r) addr(R);
    while (L > q.l) addl(L);
    while (R > q.r) eraser(R);
    while (L < q.l) erasel(L);
    result[q.p] = res;
  }
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  n = in_int();
  m = in_int();
  Size = sqrt(n);
  FOR(i, 0, n) a[i] = in_int();
  memcpy(pos, a, sizeof(int) * n);
  sort(pos, pos + n);
  auto lst = unique(pos, pos + n);
  FOR(i, 0, n) a[i] = lower_bound(pos, lst, a[i]) - pos;
  FOR(i, 0, m) query[i].get();
  sort(query, query + m);
  solve();
  FOR(i, 0, m) printf("%d\n", result[i]);
}

https://acm.uestc.edu.cn/problem/pai-zhao

这题输入时进行一遍求和预处理,然后就可以直接做差得子序列和,也可以判断最优解也只需根据前缀和,可用单调队列的思想找最优解,线性复杂度内解决问题(这题时限应该是有意没卡暴力)。由于只需要一个最优值,所以可以直接把多余的值pop(stl的priority_queue无此功能),保持队列内元素的单调性,超出长度限制的元素也要pop掉。可以两端pop的容器有list和deque,本来当时想由于不需要随机访问,用链表应该比较好的,但是对比了一下时间就打脸了。

(注:该方法也可以求矩形范围的最值,对每行求一次,得到一个最值矩阵,再对其每列求一次即可,参考codeforces-1195E

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <bits/stdc++.h>
using namespace std;
int main() {
  int n, m;
  scanf("%d%d", &n, &m);
  long long sum[n + 1];
  for (int i = 1; i <= n; ++i) {//初始化求和,保留 sum[0],可以直接使用 sum[i]-sum[0]表示从第一个到第 i个的和,无需单独加代码
    scanf("%lld", &sum[i]);
    sum[i] += sum[i - 1];
  }
  deque<int> begin;
  long long result = LLONG_MIN;
  begin.push_front(0);
  for (int i = 1; i <= n; ++i) {
    int t = begin.size();//避免多次调用 begin.size(),讲道理 deque 的size()复杂度为 1,但是加了这句之后还是快了一丢丢
    while ( --t && sum[begin.front()] > sum[i]) begin.pop_front();//排出 sum 值大的元素,至少保留一个元素
    if (i - begin.back() > m) begin.pop_back();//如果长度超限,pop 末尾元素
    long long dsum = sum[i] - sum[begin.back()];
    if (dsum > result) result = dsum;//计算并更新最大值
    if (sum[begin.front()] > sum[i]) begin.pop_front();//在之前保留的元素和 i之间选择下一步的最优解
    begin.push_front(i);
  }
  printf("%lld\n", result);
}

https://acm.uestc.edu.cn/problem/pai-ming

这是一个动态计算排名的题。各队成绩放数组里就好,但是排名,在堆里面反复find队1肯定复杂度肯定是过大的。不过,好在我们只关心队1的名次,因此可以只排序成绩比队1好的队伍,不断增删维护,使的队1一直在排序的末尾,名次可直接用size读取。STL中multiset是最合适的容器,priority_queue虽然更快但是功能不足。

 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
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct T {//存储过题数与罚时,重载运算符以比较成绩
  ll pe = 0;
  int so = 0;
  inline void add(int n) { ++this->so, this->pe += n; }
  bool friend operator<(T a, T b) {
    return a.so == b.so ? a.pe < b.pe : a.so > b.so;
  }
};
int main() {
  int n, m;
  scanf("%d%d", &n, &m);
  T team[n + 1];
  multiset<T> trank;
  trank.insert(team[1]);//默认加入队 1
  while (m--) {
    int t, p;
    scanf("%d%d", &t, &p);
    T tmp = team[t];
    team[t].add(p);
    auto itend = trank.end();
    if (t == 1) {
      auto it = trank.lower_bound(team[t]);
      do  trank.erase(it++);
      while (it != trank.end());//队 1名次上升后,删除后面的队伍
      trank.insert(team[t]);
    } else if (team[t] < team[1]) {
      trank.insert(team[t]);
      if (tmp < team[1]) {
        trank.erase(trank.lower_bound(tmp));//删除过期的数据
      }
    }
    printf("%lu\n", trank.size());
  }
}

https://acm.uestc.edu.cn/problem/chong-hai-dai

这题是在一个环中取互不相邻的数,求其最大值。如果直接采用贪心的策略,取最大值a[tmp]后删除a[tmp]、a[a[tmp].l]和a[a[tmp].r],则可能存在为了一个最大的元素放弃两个稍小元素的情况,并不能得到最优解。因此我们需要在贪心的过程中有“反悔”,选择之前元素的两个相邻元素的机会。因此,我们可以加入a[a[tmp].l].value+a[a[tmp].r].value-a[tmp].value,下一次选择该元素即相当于反悔选择a[tmp],改为选择a[a[tmp].l]和a[a[tmp].r],按这个策略贪心,取完m元素求和即可。找最大值可以使用priority_queue。

 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
#include <bits/stdc++.h>
using namespace std;
const int visited = 10001;//取范围外的值作为已删去的标记
struct node {
  int value, l, r;
  node() {}
  node(int value, int pos) {
    this->value = value;
    this->l = pos - 1, this->r = pos + 1;
  }
};
struct q {
  int value, pos;
  bool operator<(q m) const { return this->value < m.value; }
};//数组存储各点的值和左右元素下标,队列存储值和对应的下标
int main() {
  int m, n;
  scanf("%d%d", &n, &m);
  if (n < m * 2) {//为了取 m个不相邻元素,至少需要 2 * m 个元素
    puts("Error!");
    return 0;
  }
  node a[n];
  priority_queue<q> pq;
  for (int i = 0; i < n; ++i) {
    int tmp;
    scanf("%d", &tmp);
    a[i] = node(tmp, i);
    pq.push({tmp, i});
  }
  a[0].l = n - 1, a[n - 1].r = 0;
  int ans = 0;
  while (m--) {
    q tmp;
    do {
      tmp = pq.top();
      pq.pop();
    } while (a[tmp.pos].value == visited);//在队列中跳过已删除的元素
    ans += tmp.value;
    tmp.value = a[tmp.pos].value =
        a[a[tmp.pos].l].value + a[a[tmp.pos].r].value - a[tmp.pos].value;
    pq.push(tmp);
    a[a[tmp.pos].l].value = a[a[tmp.pos].r].value = visited;
    a[tmp.pos].l = a[a[tmp.pos].l].l, a[tmp.pos].r = a[a[tmp.pos].r].r;
    a[a[tmp.pos].l].r = a[a[tmp.pos].r].l = tmp.pos;//删除元素,调整相关元素的左右元素下标,形成新的环
  }
  printf("%d", ans);
}

https://acm.uestc.edu.cn/problem/dui-da-an

该题是在线地判断一些数之间的关系是否矛盾,判断条件为奇偶。每个数有两种可能,每行数据也对应两种可能。对此可以把每个数的两种可能作为两个点,显然该两点是矛盾的。然后根据每行数据成立的两种可能,对并查集进行两次连接操作,再检查矛盾的两点是否被连在同一个并查集中,如果在,则说明条件存在矛盾。

 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
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000001;
int fa[2 * maxn];
int dep[2 * maxn];
//简单的秩优化并查集
int find(int x) {
  if (fa[x] == x)
    return x;
  return fa[x] = find(fa[x]);
}
void connect(int x, int y) {//函数名 union 被stdc++.h 占了。。
  x = find(x), y = find(y);
  if (dep[x] > dep[y])
    fa[y] = x;
  else {
    fa[x] = y;
    if (dep[x] == dep[y])
      ++dep[y];
  }
}
int main() {
  int n, m;
  scanf("%d%d", &n, &m);
  for (int i = 0; i < 2 * maxn; ++i)//初始化父节点数组
    fa[i] = i;
  int i;
  for (i = 0; i < m; ++i) {
    int a, b;
    char l[5];
    scanf("%d%d%s", &a, &b, l);
    if (*l == 'e') {//判断字符串,为偶说明相同,同侧相连即可,为奇则两侧交叉相连。
      connect(a - 1, b);
      connect(a - 1 + maxn, b + maxn);
      if (find(a - 1) == find(a - 1 + maxn)) {
        printf("%d", i);
        return 0;
      }
    } else {
      connect(a - 1, b + maxn);
      connect(a - 1 + maxn, b);
      if (find(a - 1) == find(a - 1 + maxn)) {
        printf("%d", i);
        return 0;
      }
    }
  }
  puts("ORZQHQH");
}

https://acm.uestc.edu.cn/problem/wo-de-ti-mian-zui-jian-dan-kuai-lai-zuo

这题是一道比较复杂的线段树题目。要求求最长等差数列的长度,既然是等差,我们就可以维护各数之间的差,这样询问区间l到r的最长等差序列长度,也就是询问l+1到r的最长等值序列长度+1。

  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
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
#include <bits/stdc++.h>
using namespace std;
struct tnode {//除了最长相等序列长度之外,还要保存节点两端的值以及两端的相等序列长度,以便在合并时判断两边的等值序列是否会相连形成更长的等值数列
  int l, r,  ll = 1, rl = 1, ml = 1;
  long long lv = 0, rv = 0, mark=0;
  inline tnode() {}
  inline tnode(int l, int r) { this->l = l, this->r = r; }
  inline void change(int add) {
    this->lv += add;
    this->rv += add;
    this->mark += (long long)add;
  }
  inline void markdel() { this->mark = 0; }
  inline int mid() { return (this->l + this->r) >> 1; }
  void merge(tnode lc, tnode rc) {//关键步骤,要考虑各种情况,确保得到的各变量正确(除了 l=1 的节点的左端点,因为查询时找的是 l+1 到r 的最长等值序列,不会涉及到该值)
    this->lv = lc.lv, this->rv = rc.rv;
    this->ll = lc.ll, this->rl = rc.rl;
    this->ml = max(lc.ml, rc.ml);
    if (lc.rv == rc.lv) {
      if (this->ml < lc.rl + rc.ll) this->ml = lc.rl + rc.ll;
      if (lc.len() == lc.ll) this->ll += rc.ll;
      if (rc.len() == rc.rl) this->rl += lc.rl;
    }
  }
  inline int len() { return r - l + 1; }
};

tnode node[400000];
tnode ans;
bool ans_add;
void buildtree(int l, int r, int pos = 1) {
  tnode &n = node[pos];
  n = tnode(l, r);
  if (l == r) return;
  int lc = pos << 1, rc = pos << 1 | 1;
  buildtree(l, n.mid(), lc);
  buildtree(n.mid() + 1, r, rc);
  n.merge(node[lc], node[rc]);
}

inline void pushdown(int pos) {
  tnode &n = node[pos];
  node[pos << 1].change(n.mark);
  node[pos << 1 | 1].change(n.mark);
  n.markdel();
}

void query(int l, int r, int pos = 1) {
  tnode &n = node[pos];
  int lc = pos << 1, rc = pos << 1 | 1;
  if (l > n.r || r < n.l) return;
  if (l <= n.l && r >= n.r) {//递归查找过程中左边的点一定先被找到,所以可以把找到的第一个点存下来,后面找到的节点依次并上去,得到最终结果
    if (ans_add)
      ans.merge(ans, n);
    else {
      ans = n;
      ans_add = 1;
    }
  } else {
    if (n.mark) pushdown(pos);
    query(l, r, pos << 1);
    query(l, r, pos << 1 | 1);
  }
}

void update(int l, int r, int add, int pos = 1) {
  tnode &n = node[pos];
  if (l > n.r || r < n.l) return;
  if (l <= n.l && r >= n.r)
    n.change(add);
  else {
    int lc = pos << 1, rc = pos << 1 | 1;
    if (n.mark) pushdown(pos);
    update(l, r, add, lc);
    update(l, r, add, rc);
    n.merge(node[lc], node[rc]);
  }
}
int main() {
  int n, q;
  scanf("%d%d", &n, &q);
  buildtree(1, n);
  while (q--) {
    int op;
    int l, r;
    scanf("%d%d%d", &op, &l, &r);
    if (op) {
      if (l == r) {
        printf("1\n");
        continue;
      }
      ans_add = 0;//重置标记
      query(l + 1, r);
      printf("%d\n", ans.ml + 1);
    } else {//区间 l~r+1 会受到影响,各段受到的影响不同,为了方便,分多次调用 update
      int a, k, p;
      scanf("%d%d%d", &a, &k, &p);
      update(l, l, a);
      if (p > l) update(l + 1, p, k);
      if (p < r) update(p + 1, r, -k);
      update(r + 1, r + 1, -a - (2 * p - l - r) * k);
    }
  }
}

https://acm.uestc.edu.cn/problem/shu-li-tong-ji

这题是最基本的带标记线段树,基本的加法操作,维护和与最值(极差为最值之差)。由于基础比较差,之前对基本的树形结构都没有动手完整地写过,对递归和DFS的理解也不深。这次线段树写得非常难受。反复看过很多教程,又重新仔细看了一下基本的二叉树,踩了很多坑才动手把这题写出来。

 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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll ans_sum, ans_max, ans_min;
struct tnode {//对于节点的基本操作:初始化,更新,删除标记,合并等。出于方便还写了区间长度和中点。延迟更新标记是比较一个有些抽象的概念,但是在优化复杂度的过程中不可或缺,当操作施加于整个区间时,由于加法完全满足结合律,如果后面不会查询到其子区间,就不用把更新往下传,一组数据中往往大部分的节点更新都是可以不用传到底的。但是如果查询到子节点,就必须传递更新信息,否则结果将错误。于是在不知道是否会查询子节点的情况下,暂时先保存更新信息,后面查询的时候再按需传递更新
  ll l, r, sum = 0, max = 0, min = 0, mark = 0;
  inline tnode() {}
  inline tnode(ll l, ll r) { this->l = l, this->r = r; }
  inline tnode(ll sum, ll max, ll min) {
    this->sum = sum, this->max = max, this->min = min;
  }
  inline void add(ll add) {
    this->sum += add * (this->r - this->l + 1);
    this->max += add;
    this->min += add;
    this->mark += add;
  }
  inline void markdel() { this->mark = 0; }
  inline ll mid() { return (this->l + this->r) >> 1; }
  inline void merge(tnode lc, tnode rc) {
    this->sum = lc.sum + rc.sum + this->mark * (this->r - this->l + 1);
    this->max = std::max<ll>(lc.max, rc.max) + this->mark;
    this->min = std::min<ll>(lc.min, rc.min) + this->mark;
  }
};

tnode node[4000000];

inline void buildtree(ll l, ll r, ll pos = 1) {//递归建树,以 1为根节点
  tnode &n = node[pos];
  n = tnode(l, r);
  if (l == r) return;
  ll lc = pos << 1, rc = pos << 1 | 1;
  buildtree(l, n.mid(), lc);
  buildtree(n.mid() + 1, r, rc);
}

inline void pushdown(ll pos) {//把当前节点的标记推送到下层节点,然后清除标记,在查询时调用
  tnode &n = node[pos];
  node[pos << 1].add(n.mark);
  node[pos << 1 | 1].add(n.mark);
  n.markdel();
}

void query(ll l, ll r, ll pos = 1) {//容易写错的一步。查询操作,从大区间逐层向下递归,最终得出结果
  tnode &n = node[pos];
  if (l > n.r || r < n.l) return;
  if (l <= n.l && r >= n.r) {
    ans_sum += n.sum;
    ans_max = max<ll>(ans_max, n.max);
    ans_min = min<ll>(ans_min, n.min);
  } else {
    if (n.mark) pushdown(pos);
    query(l, r, pos << 1);
    query(l, r, pos << 1 | 1);
  }
}

void update(ll l, ll r, ll add, ll pos = 1) {//加法操作有结合律,更新时不需要 pushdown
  tnode &n = node[pos];
  if (l > n.r || r < n.l) return;
  if (l <= n.l && r >= n.r)
    n.add(add);
  else {
    ll lc = pos << 1, rc = pos << 1 | 1;
    update(l, r, add, lc);
    update(l, r, add, rc);
    n.merge(node[lc], node[rc]);
  }
}
int main() {
  ll n, q;
  scanf("%lld%lld", &n, &q);
  buildtree(1, n);
  while (q--) {
    int op;
    ll l, r;
    scanf("%d%lld%lld", &op, &l, &r);
    if (op == 1) {
      ll k;
      scanf("%lld", &k);
      update(l, r, k);
    } else {
      ans_sum = 0;
      ans_max = LLONG_MIN;
      ans_min = LLONG_MAX;
      query(l, r);
      if (op == 2)
        printf("%lld\n", ans_sum);
      else
        printf("%lld\n", ans_max - ans_min);
    }
  }
}

https://acm.uestc.edu.cn/problem/zhan-zheng

这是一个典型的用01字典树解决抑或最值问题的题目。我们把所需的数字的二进制从高到低位地保存在字典树中,查询最值的时候根据贪心思想,即可轻松地找到最值。01字典树可以是一棵二叉树,但是这样写要占用n个int的内存,超出了允许的范围,因此我们只添加存在的节点

 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
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500000;
int poi[30 * MAXN], ch[30 * MAXN][2], co[30 * MAXN][2], num = 0;//按照最大需求量开数组,另开一个数组记录每个节点下数字的个数,还有一个数组记录最后的节点对应的整个数字(这个数组不要也行,可在查询函数中边查边算)
void insert(int x) {//插入数,从高到低
  int u = 0;
  for (int i = 29; i >= 0; --i) {
    int tmp = x >> i & 1;
    ++co[u][tmp];
    if (!ch[u][tmp]) {
      ch[u][tmp] = ++num;
    }
    u = ch[u][tmp];
  }
  poi[u] = x;
}

void erase(int x) {//这里只操作记录个数的数组,并以该数组作为判断是否有数的依据
  int u = 0;
  for (int i = 29; i >= 0; --i) {
    int tmp = x >> i & 1;
    --co[u][tmp];
    u = ch[u][tmp];
  }
}
int find_max(int x) {//贪心,优先不同值
  int u = 0;
  for (int i = 29; i >= 0; --i) {
    int tmp = x >> i & 1;
    if (co[u][tmp ^ 1])
      u = ch[u][tmp ^ 1];
    else
      u = ch[u][tmp];
  }
  return poi[u];
}
int find_min(int x) {//贪心,优先相同值
  int u = 0;
  for (int i = 29; i >= 0; --i) {
    int tmp = x >> i & 1;
    if (co[u][tmp])
      u = ch[u][tmp];
    else
      u = ch[u][tmp ^ 1];
  }
  return poi[u];
}
int main() {
  int n;
  scanf("%d", &n);
  while (n--) {
    int o, v;
    scanf("%d%d", &o, &v);
    switch (o) {
      case 1:
        insert(v);
        break;
      case 2:
        erase(v);
        break;
      case 3:
        printf("%d %d\n", find_min(v) ^ v, find_max(v) ^ v);
    }
  }
}