# Educational Codeforces Round 97 (CF1437) 题解

# Problem A - Marketing Scheme (opens new window)

提示

如果r2lr\geq2l,会如何?

解法

如果r2lr\geq2l,则我们至少需要覆盖区间[l,2l][l,2l]。显然,我们需要a>la>l,因为这一区间的长度已经为l+1l+1。如果l<a2ll<a\leq2l,则模aa的余数中至多有ll个满足a2\geq\frac{a}{2},因此这一长度为l+1l+1的区间中,至少有一个数模aa的余数<a2<\frac{a}{2}。但如果取a>2la>2l,则llaa的余数必然<a2<\frac{a}{2}。从而,在r2lr\geq2l时,不存在满足条件的aa

反过来,如果r<2lr<2l,我们总可以取a=2la=2l,易知它是一个满足题意的答案。

所以,这道题我们只需要判断是否满足r<2lr<2l即可。

时间复杂度为O(1)O(1)

参考代码 (Python 3)
def read_int():
    return int(input())


def read_ints():
    return map(int, input().split(' '))


t = read_int()
for case_num in range(t):
    l, r = read_ints()
    print('YES' if l * 2 > r else 'NO')
1
2
3
4
5
6
7
8
9
10
11
12

# Problem B - Reverse Binary Strings (opens new window)

提示

经过一次翻转,00001111对的数目会减少多少?

解法

在一次翻转中,00001111对的数目至多减少22。所以我们只需要统计出这样的数对的数目即可。

时间复杂度为O(N)O(N)

参考代码 (Python 3)
def read_int():
    return int(input())


def read_ints():
    return map(int, input().split(' '))


t = read_int()
for case_num in range(t):
    n = read_int()
    s = input()
    tot = 0
    for i in range(n - 1):
        if s[i] == s[i + 1]:
            tot += 1
    print((tot + 1) // 2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Problem C - Chef Monocarp (opens new window)

提示
  1. 我们不会用到超过2n2n的时间。
  2. 数组的原始顺序没有影响。
解法

我们不会用到t>2nt>2n的时间,这是因为a[i]na[i]\leq n,所以即使我们是在t=nt=n时刻熄灭第一个炉子,也来得及在t=2n1t=2n-1时刻熄灭最后一个炉子。因此,如果我们用到了t>2nt>2n的时间,就一定可以通过改成更早的时间来降低总的不满度。事实上,这一上界还可以进一步降低,但2n2n已经可以满足要求。

首先,我们对数组进行排序,然后进行动态规划。令dp[t]dp[t]表示在tt时刻熄灭最后一个炉子的总不满度。

因为时间是单调的,我们考虑下一个炉子的时候,只会从小于当前时刻的时刻向当前时刻转移。对于第ii个炉子,我们进行这样的更新:令dp[t]=mint<t(dp[t])+ta[i]dp[t]=\min_{t'<t}(dp[t'])+|t-a[i]|。为了避免重复计算,我们可以维护一个前缀最小值lolo

总时间复杂度为O(N2)O(N^2)

(Python 3)
def read_int():
    return int(input())


def read_ints():
    return map(int, input().split(' '))


inf = int(1e9)
t = read_int()
for case_num in range(t):
    n = read_int()
    a = list(read_ints())
    a.sort()
    dp = [0 for _ in range(n * 2 + 1)]
    for t in a:
        ndp = [inf for _ in range(n * 2 + 1)]
        lo = inf
        for i in range(n * 2):
            lo = min(lo, dp[i])
            ndp[i + 1] = min(ndp[i + 1], lo + abs(i + 1 - t))
        dp = ndp
    print(min(dp))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Problem D - Minimal Height Tree (opens new window)

提示

贪心。

解法

我们可以贪心地分配节点。除非当前数字比上一个数字小,否则我们总是将其连接到同一个父节点。否则,我们移动到下一个父节点(它可能在树的下一层)。

时间复杂度为O(N)O(N)

参考代码 (C++)
#include <cstdio>
#include <iostream>
#include <vector>

using namespace std;

template <typename T> void read(T &x) {
  x = 0;
  char c = getchar();
  T sig = 1;
  for (; !isdigit(c); c = getchar())
    if (c == '-')
      sig = -1;
  for (; isdigit(c); c = getchar())
    x = (x << 3) + (x << 1) + c - '0';
  x *= sig;
}

class Solution {
public:
  void solve() {
    int n;
    read(n);
    int height = 0;
    vector<int> level = {1};
    int col = 0, first, last = 0;
    read(first);
    for (int i = 0; i < n - 1; ++i) {
      int u;
      read(u);
      if (u < last) {
        col++;
        if (col == level[height]) {
          height++;
          col = 0;
        }
      }
      if (level.size() <= height + 1)
        level.emplace_back(0);
      level[height + 1]++;
      last = u;
    }
    printf("%d\n", (int)level.size() - 1);
  }
};

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  int t;
  read(t);
  while (t--) {
    Solution solution = Solution();
    solution.solve();
  }
}
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

# Problem E - Make It Increasing (opens new window)

提示
  1. 我们可以在数组aabb的头尾各添加一个哨兵位,这样就可以把原问题分解为若干子问题。
  2. 每个子问题实质上是最长上升子序列(LIS)。
解法

第一步,我们将原问题分解为子问题。每个子问题包括四个参数,llrr代表区间起点和终点,lolo代表区间起点的最小取值,hihi代表区间终点的最大取值。

对于每一子问题,我们考虑以下三种情况:

  • l>rl>r,也即空区间的情形,对应的结果显然为00
  • l=rl=r,也即区间长度为11的情形,对应的结果取决于a[l]a[l]是否在[lo,hi][lo,hi]范围内。
  • l<rl<r。在这一情形中,我们需要找到区间内的最长上升子序列。不过,这与经典的LIS问题有两点区别。首先,如果当前数字不在其合法范围内(这一范围可以由lolohihi计算得到,为[lo+il,hir+i][lo+i-l,hi-r+i]),它就不能被加入到LIS中。另一区别是,我们不能直接使用a[i]a[i]。考虑这一例子,[2,5,3,1][2,5,3,1]。我们可以用[2,3][2,3]作为LIS吗?答案是否定的,因为如果这样的话,中间的数字就无法安排了。我们如何将这一距离因素考虑进来呢?方法是,用a[i]ia[i]-i代替a[i]a[i],同时,求取最长不下降子序列而非最长上升子序列。通过i-i,我们就消去了不同位置间的距离差异。

最长上升子序列(最长不下降子序列)是一个经典的动态规划问题,并可以利用二分搜索进一步优化。对于长度为LL的区间,其时间复杂度为O(LlogL)O(L\log L)

因此,最后的总时间复杂度为O(NlogN)O(N\log N)

参考代码 (C++)
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <vector>
#define INF 0x3f3f3f3f

using namespace std;

template <typename T> void read(T &x) {
  x = 0;
  char c = getchar();
  T sig = 1;
  for (; !isdigit(c); c = getchar())
    if (c == '-')
      sig = -1;
  for (; isdigit(c); c = getchar())
    x = (x << 3) + (x << 1) + c - '0';
  x *= sig;
}

class Solution {
public:
  void solve() {
    int n, k;
    read(n), read(k);
    vector<int> a(n + 2), b(k + 2);
    a[0] = -INF, a[n + 1] = INF;
    b[0] = 0, b[k + 1] = n + 1;
    for (int i = 1; i <= n; ++i)
      read(a[i]);
    for (int i = 1; i <= k; ++i) {
      read(b[i]);
      if (a[b[i]] - a[b[i - 1]] < b[i] - b[i - 1]) {
        printf("-1");
        return;
      }
    }
    int ans = 0;
    auto handle = [&](int l, int r, int lo, int hi) {
      if (l > r)
        return 0;
      if (l == r)
        return (a[l] >= lo && a[l] <= hi) ? 0 : 1;
      vector<int> LIS;
      for (int i = l; i <= r; ++i) {
        int clo = lo + i - l, chi = hi - r + i;
        if (a[i] < clo || a[i] > chi)
          continue;
        int pos = upper_bound(LIS.begin(), LIS.end(), a[i] - i) - LIS.begin();
        if (pos >= LIS.size())
          LIS.emplace_back(a[i] - i);
        else
          LIS[pos] = a[i] - i;
      }
      return r - l + 1 - (int)LIS.size();
    };
    for (int i = 1; i <= k + 1; ++i)
      ans += handle(b[i - 1] + 1, b[i] - 1, a[b[i - 1]] + 1, a[b[i]] - 1);
    printf("%d", ans);
  }
};

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  Solution solution = Solution();
  solution.solve();
}
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

# Problem F - Emotional Fishermen (opens new window)

提示

如果当前集合中的最大值为aa,集合中的次大值最大为多少?

解法

本题中的关键点是:如果当前集合中的最大值为maxmax,次大值为secondsecond,则它们一定满足2second<max2\cdot second<max

首先,我们对数组进行排序。接下来,我们可以用双指针的方法为每一个a[i]a[i]找出pre[i]pre[i],也即能够存在于以a[i]a[i]为最大值中的集合的元素的最大下标。

接下来,我们进行动态规划。令dp[i]dp[i]代表最大元素为a[i]a[i]的合法排列的数目。一共有两种类型的转移:

  1. dp[i]dp[i]dp[i]dp[i]。这一转移要求我们从[0,pre[i]][0,pre[i]]中选择一个元素加入排列。我们可以通过当前集合中的元素个数来求出可能的选择数。
  2. dp[j]dp[j]dp[i]dp[i],其中jpre[i]j\leq pre[i]。为了加速计算这一类转移,我们可以在每次循环后计算dpdp数组的前缀和。

初始状态为dp[i]=1dp[i]=1,也即第ii个集合中只包含了a[i]a[i]这一个元素。

总时间复杂度为O(N2)O(N^2)

参考代码 (C++)
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <vector>
#define MOD 998244353

using namespace std;
typedef long long ll;

template <typename T> void read(T &x) {
  x = 0;
  char c = getchar();
  T sig = 1;
  for (; !isdigit(c); c = getchar())
    if (c == '-')
      sig = -1;
  for (; isdigit(c); c = getchar())
    x = (x << 3) + (x << 1) + c - '0';
  x *= sig;
}

class Solution {
public:
  void solve() {
    int n;
    read(n);
    vector<int> a(n);
    for (int i = 0; i < n; ++i)
      read(a[i]);
    sort(a.begin(), a.end());
    if (a[n - 2] * 2 > a[n - 1]) {
      printf("0");
      return;
    }

    vector<int> pre(n, -1);
    int l = n - 2;
    for (int r = n - 1; r >= 0; --r) {
      while (l >= 0 && a[l] * 2 > a[r])
        l--;
      if (l >= 0)
        pre[r] = l;
    }

    ll ans = 0;
    vector<ll> dp(n, 1), S(n + 1);
    for (int i = 1; i <= n; ++i)
      S[i] = i;
    for (int i = 1; i < n; ++i) {
      vector<ll> ndp(n, 0);
      for (int j = 0; j < n; ++j) {
        // Case 1: j to j
        int left = pre[j] == -1 ? 0 : pre[j] + 1;
        left -= i - 1;
        if (left > 0)
          ndp[j] = (ndp[j] + dp[j] * left % MOD);

        // Case 2: smaller to j
        if (pre[j] != -1)
          ndp[j] = (ndp[j] + S[pre[j] + 1]) % MOD;
      }
      for (int j = 1; j <= n; ++j)
        S[j] = (S[j - 1] + ndp[j - 1]) % MOD;
      dp = move(ndp);
    }
    printf("%lld", S[n]);
  }
};

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  Solution solution = Solution();
  solution.solve();
}
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

# Problem G - Death DBMS (opens new window)

提示

我们需要找到一个给定的字符串中所有名字的出现情况。是时候登场了,AC自动机!

解法

这是一道非常典型的AC自动机的题目。因为可能有重复的名字,所以我们可以用set来保存相同名字的人的可疑度,这样方便我们获取最大值。

对于每一次询问,在常规遍历结束后,我们还需要沿着fail指针向上遍历所有节点,来获取所有完整出现的名字的最大可疑度。

参考代码 (C++)
#include <iostream>
#include <queue>
#include <set>
#include <unordered_map>
#include <vector>

using namespace std;
struct Node {
  int idx = -1, fail = 0, children[26]{};
};

int main() {
  int n, q;
  cin >> n >> q;

  vector<string> names(n + 1);
  unordered_map<string, int> dict;
  unordered_map<int, int> id_dict;
  vector<set<pair<int, int>, greater<>>> heaps;
  vector<int> suspicion(n + 1);
  suspicion[0] = -1;
  vector<Node> nodes = {Node{}};
  int idx = 0;
  for (int i = 1; i <= n; ++i) {
    cin >> names[i];
    if (dict.count(names[i])) {
      heaps[dict[names[i]]].emplace(0, i);
      id_dict[i] = dict[names[i]];
      continue;
    }
    dict[names[i]] = idx;
    id_dict[i] = idx;
    heaps.push_back({{0, i}});
    int p = 0;
    for (char c : names[i]) {
      if (!nodes[p].children[c - 'a']) {
        nodes[p].children[c - 'a'] = nodes.size();
        nodes.emplace_back(Node{});
      }
      p = nodes[p].children[c - 'a'];
    }
    nodes[p].idx = idx++;
  }

  queue<int> que, visited;
  vector<bool> vis(nodes.size());
  for (int u : nodes[0].children)
    if (u)
      que.push(u);
  while (!que.empty()) {
    int u = que.front();
    que.pop();
    for (int i = 0; i < 26; ++i) {
      auto &v = nodes[u].children[i];
      if (v) {
        nodes[v].fail = nodes[nodes[u].fail].children[i];
        que.push(v);
      } else
        v = nodes[nodes[u].fail].children[i];
    }
  }

  string output;

  auto query = [&](string &s) {
    int ans = -1;
    int p = 0;
    int idx = 0;
    while (idx < s.size()) {
      char c = s[idx];
      if (nodes[p].children[c - 'a']) {
        p = nodes[p].children[c - 'a'];
        if (!vis[p]) {
          vis[p] = true;
          que.push(p);
        }
        idx++;
      } else {
        p = nodes[p].fail;
        if (!p)
          idx++;
      }
    }
    while (!que.empty()) {
      int u = que.front();
      que.pop();
      visited.push(u);
      if (nodes[u].idx != -1)
        ans = max(ans, heaps[nodes[u].idx].begin()->first);
      if (nodes[u].fail && !vis[nodes[u].fail]) {
        vis[nodes[u].fail] = true;
        que.push(nodes[u].fail);
      }
    }
    while (!visited.empty()) {
      vis[visited.front()] = false;
      visited.pop();
    }
    output += to_string(ans) + "\n";
  };

  while (q--) {
    int t;
    cin >> t;
    if (t == 1) {
      int i, x;
      cin >> i >> x;
      heaps[id_dict[i]].erase({suspicion[i], i});
      suspicion[i] = x;
      heaps[id_dict[i]].emplace(suspicion[i], i);
    } else {
      string s;
      cin >> s;
      query(s);
    }
  }

  cout << output;
}
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
111
112
113
114
115
116
117
118