Educational Codeforces Round 97 (CF1437)
Problem A - Marketing Scheme
提示
如果,会如何?
解法
如果,则我们至少需要覆盖区间。显然,我们需要,因为这一区间的长度已经为。如果,则模的余数中至多有个满足,因此这一长度为的区间中,至少有一个数模的余数。但如果取,则模的余数必然。从而,在时,不存在满足条件的。
反过来,如果,我们总可以取,易知它是一个满足题意的答案。
所以,这道题我们只需要判断是否满足即可。
时间复杂度为。
参考代码 (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')
Problem B - Reverse Binary Strings
提示
经过一次翻转,或对的数目会减少多少?
解法
在一次翻转中,或对的数目至多减少。所以我们只需要统计出这样的数对的数目即可。
时间复杂度为。
参考代码 (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)
Problem C - Chef Monocarp
提示
- 我们不会用到超过的时间。
- 数组的原始顺序没有影响。
解法
我们不会用到的时间,这是因为,所以即使我们是在时刻熄灭第一个炉子,也来得及在时刻熄灭最后一个炉子。因此,如果我们用到了的时间,就一定可以通过改成更早的时间来降低总的不满度。事实上,这一上界还可以进一步降低,但已经可以满足要求。
首先,我们对数组进行排序,然后进行动态规划。令表示在时刻熄灭最后一个炉子的总不满度。
因为时间是单调的,我们考虑下一个炉子的时候,只会从小于当前时刻的时刻向当前时刻转移。对于第个炉子,我们进行这样的更新:令。为了避免重复计算,我们可以维护一个前缀最小值。
总时间复杂度为。
(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))
Problem D - Minimal Height Tree
提示
贪心。
解法
我们可以贪心地分配节点。除非当前数字比上一个数字小,否则我们总是将其连接到同一个父节点。否则,我们移动到下一个父节点(它可能在树的下一层)。
时间复杂度为。
参考代码 (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();
}
}
Problem E - Make It Increasing
提示
- 我们可以在数组和的头尾各添加一个哨兵位,这样就可以把原问题分解为若干子问题。
- 每个子问题实质上是最长上升子序列(LIS)。
解法
第一步,我们将原问题分解为子问题。每个子问题包括四个参数,和代表区间起点和终点,代表区间起点的最小取值,代表区间终点的最大取值。
对于每一子问题,我们考虑以下三种情况:
- ,也即空区间的情形,对应的结果显然为。
- ,也即区间长度为的情形,对应的结果取决于是否在范围内。
- 。在这一情形中,我们需要找到区间内的最长上升子序列。不过,这与经典的LIS问题有两点区别。首先,如果当前数字不在其合法范围内(这一范围可以由和计算得到,为),它就不能被加入到LIS中。另一区别是,我们不能直接使用。考虑这一例子,。我们可以用作为LIS吗?答案是否定的,因为如果这样的话,中间的数字就无法安排了。我们如何将这一距离因素考虑进来呢?方法是,用代替,同时,求取最长不下降子序列而非最长上升子序列。通过,我们就消去了不同位置间的距离差异。
最长上升子序列(最长不下降子序列)是一个经典的动态规划问题,并可以利用二分搜索进一步优化。对于长度为的区间,其时间复杂度为。
因此,最后的总时间复杂度为。
参考代码 (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();
}
Problem F - Emotional Fishermen
提示
如果当前集合中的最大值为,集合中的次大值最大为多少?
解法
本题中的关键点是:如果当前集合中的最大值为,次大值为,则它们一定满足。
首先,我们对数组进行排序。接下来,我们可以用双指针的方法为每一个找出,也即能够存在于以为最大值中的集合的元素的最大下标。
接下来,我们进行动态规划。令代表最大元素为的合法排列的数目。一共有两种类型的转移:
- 从到。这一转移要求我们从中选择一个元素加入排列。我们可以通过当前集合中的元素个数来求出可能的选择数。
- 从到,其中。为了加速计算这一类转移,我们可以在每次循环后计算数组的前缀和。
初始状态为,也即第个集合中只包含了这一个元素。
总时间复杂度为。
参考代码 (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();
}
Problem G - Death DBMS
提示
我们需要找到一个给定的字符串中所有名字的出现情况。是时候登场了,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;
}