Codeforces Round 950 (Div. 3)
https://codeforces.com/contest/1980
A. Problem Generator
题意:There is going to be m rounds next mouth, each of the month should be consist of "ABCDEFG", count the numebr of alphabet we should add to satisfy this requirement under a giving sequence.
总结:题比较难读懂, Each round should contain one problem..说明了每个round都应该包含这些东西,没包含就要补上去。
void solve() {
int n, m;
cin >> n >> m;
string s;
cin >> s;
array<int, 26> a{};
for (const auto& x : s) {
a[x - 'A']++;
}
int res = 0;
for (int i = 0; i <= 6; ++i) {
res += max(0, m - a[i]);
}
cout << res << '\n';
}
B. Choosing Cubes
题意:查看要被删除的数在非升序排序后是否一定会被删除。
思路:排序,分3种情况讨论。
void solve() {
int n, m, k;
cin >> n >> m >> k;
vector<int> a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
int s = a[m];
sort(a.rbegin(), a.rend() - 1);
if (a[k] > s) {
cout << "NO\n";
}
else if (a[k] < s) {
cout << "YES\n";
}
else if (a[k] == s) {
if (k < n && a[k + 1] == s) {
cout << "MAYBE\n";
}
else {
cout << "YES\n";
}
}
}
C. Sofia and the Lost Operations
题意:给定序列a和b和d,问能否通过按序列d中的数按顺序对序列a进行操作,最后将序列a转为序列b。其中每次操作可以任选一个数将a[k]变为d[j].
思路:一次遍历序列d,看是否d中的所有的数都能被用掉,并且使用完后序列a已经完全变成了序列b.如果d中某个数在b中没出现,说明该操作是冗余操作,在冗余操作后必须有一个可以进行的操作,来将冗余操作使用掉。
总结:问题还是出在了语言上,理解错题目3次。第1次以为b中操作可以无序,第2次以为a中操作必须有序。第3次好好看了看才看懂题目。应该慢慢读题目的。
void solve() {
int n;
cin >> n;
vector<int> a(n);
vector<int> b(n);
for (auto& x : a){
cin >> x;
}
map<int, int> diff;
for (int i = 0; i < n; ++i){
cin >> b[i];
if (a[i] != b[i]){
diff[b[i]] ++;
}
if (!diff.count(b[i])){
diff[b[i]] = 0;
}
}
int m;
cin >> m;
bool ok = false;
for (int i = 0; i < m; ++i){
int x;
cin >> x;
if (diff.count(x) && diff[x] > 0){
ok = true;
diff[x] --;
}
else if(!diff.count(x)){
ok = false;
}
else{
ok = true;
}
}
for (const auto& [x, y] : diff){
if (y > 0){
ok = false;
break;
}
}
cout << (ok ? "YES\n" : "NO\n");
}
D. GCD-sequence
题意:给定一个序列a,根据相邻的数的gcd构造b,问能否exactly删除a中的一个元素,使序列b非递减。
思路:求出序列b,找到第一个拐点,然后依此尝试移除跟这次比较相关联a中的元素,暴力求解。时间复杂度(n * log(n) * c)。
总结:因为重新构造的次数一定是一个常数,所以可以直接使用暴力破解的方法。一开始想的是找到谷点以后,分情况讨论移除哪个数,来判断移除该点后是否可行,发现代码量实在是太大,最后转暴力了。 做题原则1:暴力能ac,就别犹豫。。莫装逼。
void solve() {
int n;
cin >> n;
vector<int> a(n);
for (auto& x : a) {
cin >> x;
}
vector<int> b;
for (int i = 0; i < n - 1; ++i) {
b.push_back(gcd(a[i], a[i + 1]));
}
auto check = [&](int p) {
if (p < 0 || p > n - 1) {
return false;
}
auto c = a;
c.erase(c.begin() + p);
vector<int> d;
for (int i = 0; i < n - 2; ++i) {
d.push_back(gcd(c[i], c[i + 1]));
if (i && d[i] < d[i - 1]) {
return false;
}
}
return true;
};
for (int i = 0; i < n - 2; ++i) {
if (b[i] > b[i + 1]) {
if (check(i - 1) || check(i) || check(i + 1) || check(i + 2)) {
cout << "YES\n";
return;
}
else {
cout << "NO\n";
return;
}
}
}
cout << "YES\n";
}
E. Permutation of Rows and Columns
题意:给定两个矩阵a和b,并且矩阵中的数是个permutation。问矩阵a能否通过交换任意次数行和列得到矩阵b。
思路:记录a中每个元素所在的行和列,遍历b中的元素。依此统计出该元素变换到b中的所在行和列,a的行要交换到哪个行,列要交换到哪个列,如果有冲突,则No。
总结:一开始想复杂了,以为是什么构造边,然后拓扑排序,或者并查集什么的。后来想了想,只要行列交换没有冲突,那就可以了。。
void solve() {
int n, m;
cin >> n >> m;
vector<pair<int, int>> a(n * m + 1);
for (int i = 1; i <= n; ++i){
for (int j = 1; j <= m; ++j){
int x;
cin >> x;
a[x] = {i, j};
}
}
vector<vector<int>> b(n + 1, vector<int> (m + 1));
for (int i = 1; i <= n; ++i){
for (int j = 1; j <= m; ++j){
cin >> b[i][j];
}
}
vector<int> row(n + 1, -1);
vector<int> col(m + 1, -1);
for (int i = 1; i <= n; ++i){
for (int j = 1; j <= m; ++j){
const int& x = b[i][j];
int r = a[x].first;
int c = a[x].second;
if (row[r] != -1 && row[r] != i){
cout << "NO\n";
return;
}
if (col[c] != -1 && col[c] != j){
cout << "NO\n";
return;
}
row[r] = i;
col[c] = j;
}
}
cout << "YES\n";
return;
}
上面是赛时A掉的题,大概快半年没打cf了,rank2000多,在预期内。
准备补剩下的题。
F1. Field Division (easy version)
题意:给定一个矩阵和一堆喷泉fountains,alice从左边或上边出发,问alice不碰到喷泉,最多能占到多少格子(路线左下+路线格子是alice的)。再问,考虑所有的i,如果第i个喷泉给了alice,alice是否能增加格子。
思路:模拟题,对fountains按列升序排序,行降序排序。然后依此考虑每个fountain和上一步的x和y坐标,求出上一步到当前这一步alice可以占到多少格子。如果当前x>上一步的x,说明这个fountain给了alice可以为alice带来效益。
总结:前面几个题写的太慢了,不然的话这个赛时应该能写出来,不过处理这二维坐标问题和移动,确实非常抽象。。还有就是坐标计算没有转long long,溢出了。
void solve() {
int n, m, k;
cin >> n >> m >> k;
vector<pair<long long, long long>> fountains(k);
for (auto& x : fountains){
cin >> x.first >> x.second;
}
vector<int> pos(k);
iota(pos.begin(), pos.end(), 0);
sort(pos.begin(), pos.end(), [&](const int& a, const int& b){
return fountains[a].second != fountains[b].second ?
fountains[a].second < fountains[b].second : fountains[a].first > fountains[b].first;
});
vector<int> ans(k, 0);
long long res = 0;
long long lastx = 0, lasty = 0;
bool first_time = true;
for (int i = 0; i < k; ++i){
const auto&[x, y] = fountains[pos[i]];
if (first_time){
lastx = 0;
lasty = 1;
first_time = false;
}
if (y > lasty){
res += (n - lastx) * (y - lasty);
}
if (x > lastx){
ans[pos[i]] = 1;
}
lastx = max(x, lastx);
lasty = y;
}
res += (m - lasty + 1) * (n - lastx);
cout << res << '\n';
for (int i = 0; i < k; ++i){
cout << ans[i] << " \n"[i == k - 1];
}
}
F2. Field Division (hard version)
题意:比上个题增加了一个要求输出将fountain给了alice后,alice增加了exactly多少个格子。
思路:对于有贡献的fountain,想到了使用单调栈,在下一个>=当前fountain的x喷泉出现之前,这一段中的所有列都有贡献。但是对于每个列,计算贡献又不太好计算,要遍历所有的喷泉才行,如果要遍历所有的喷泉,单调栈好像又显得没有意义。
思路更新:依此考虑每个喷泉,上一个喷泉被移除后,可能增加的列是当前喷泉到上一个喷泉的列,可能增加的行是上一个喷泉的行到(到达上一个喷泉前能到达的最小的行)。并且如果要维护的当前喷泉的高度低于左边的第一个喷泉的高度,那么移除该喷泉不会增加贡献,所以可以继续维护上一个喷泉移除后的贡献。所以需要维护两个数值,一个是能到达的最小的合法行数,以及当前正在维护的喷泉下标。
总结:之前想到了单调栈,但是不需要总是考虑下一个更靠下的喷泉,因为当遇到的下一个喷泉比当前喷泉靠上,那么仍然要继续计算当前喷泉的贡献 ,并且可能要考虑是不是能到达的最小的行要往下移。一个比较抽象的面积计算题。
void solve() {
int n, m, k;
cin >> n >> m >> k;
vector<pair<int, int>> a(k);
for (auto& x : a){
cin >> x.first >> x.second;
}
long long res = 0;
vector<int> pos(k);
iota(pos.begin(), pos.end(), 0);
sort(pos.begin(), pos.end(), [&](const int i, const int j){
return a[i].second != a[j].second ? a[i].second < a[j].second : a[i].first > a[j].first;
});
int last_x = 0;
int last_y = 1;
bool first_time = true;
int max_x = 0;
int idx = pos[0];
vector<long long> ans(k);
for (const auto& p : pos){
const auto&[x, y] = a[p];
if (first_time){
first_time = false;
}
else{
ans[idx] += 1ll * (y - last_y) * (last_x - max_x);
}
res += 1ll * (n - last_x) * (y - last_y);
if (x > last_x){
idx = p;
}
//这里比较难理解,其实也是一个式子将两种情况都考虑了。一种是更新idx,一种是不更新idx。
if (checkMax(max_x, min(x, last_x))){
//idx = p;
}
last_x = max(last_x, x);
last_y = y;
}
ans[idx] += 1ll * (m - last_y + 1) * (last_x - max_x);
res += 1ll * (n - last_x) * (m - last_y + 1);
cout << res << '\n';
for (int i = 0; i < k; ++i){
cout << ans[i] << " \n"[i == k - 1];
}
}
G. Yasya and the Mysterious Tree
题意:给个树,n个节点m个操作。每次操作有两种,第一种将所有的边都异或一个值。第二种考虑添加一条权重为x的边在给定的点上,在所有可能的环中,考虑环中所有边的异或值,异或值最大的环是多少。
思路:先随便设个根节点,求出从根节点到每个点的异或权重值与每条边的奇偶性。维护一个变量来记录边的所有第一种操作的值。 对于每次查询环权重,考虑两种添加边的情况:第一种是奇偶性相同的边,此时环种的边数为偶数,那么第一种操作全部失效。 第二种是奇偶性不同的边,此时要考虑之前所有的第一种操作。两种情况取最大值即可。对于奇偶性相同的边,在字典树中查询需要先移除给定的边,为了避免造的环自己指向自己。
总结:就是树上的添加边造最大异或值环的问题,逐步分解问题。先考虑没有第一种操作的情况,此时给定一个点v和一条边权x,构造环。很容易理解,权重最大的环就是字典树中的最大异或值查询得到的值,但是在查询前要先将根节点到点v的路径权重移除。为什么要移除?假如不移除,那么在查询时就可能会得到一个(u->v) ^ (v ^ x) = x的最大值,这时环中只有一条新加的x边,v自己指向自己。理解了这种情况后,就容易理解为什么要将边按奇偶性来区分。在奇偶性相同的树中,选择任意一个点来建环,环中的边一定是偶数。反之在另一棵树中,边数一定是奇数。 而这样维护两颗树,就可以用来处理第一种操作了。
/*
* Trie(字典树)
* 设计思想:字典树设计的思想是基于动态开点的m叉树。
* 基于面向对象的编程思想,本方法尽可能多的隐藏了内部实现的细节,并且将必要的编程接口暴露在外部,并需要对这些接口进行直接的修改。
* 在该设计中,基础的方法都已经实现,建议用户根据自己的需求自己在字典树主体或者节点类型中实现对应的逻辑。
* 在该设计中:实现了基于字符串的插入删除,查询前缀数量及相同字符串数量。
* 实现了基于int型变量的二进制插入删除,以及最大异或值的查询。
*
*
* gitHub(仓库地址): https://github.com/100000000000000000000000000000000/programming-template.git
*
*/
template<const unsigned int ALPHABET_SIZE> class Trie;
/*
Trie类的节点,每个节点有若干个指针,在实例化类对象时指定。
每个节点除了指针还有两个变量来记数,分别表示经过当前节点的数据数量pass_count_和以当前节点为结尾的数据数量end_count_。
*/
template<unsigned int ALPHABET_SIZE>
class TrieNode {
friend class Trie<ALPHABET_SIZE>;
public:
TrieNode() : pass_count_(0), end_count_(0), next_{} {}
private:
unsigned int pass_count_;
unsigned int end_count_;
std::array<int, ALPHABET_SIZE> next_;
};
/*
字典树,实例化时需要传递一个非负值作为节点的指针数量,初始化必须携带起始字符,如果是int的二进制数据建树初始化列表可以输入0。
使用示例:Trie<26> trie('a'); Trie<2> trie(0);
注意:如果将数字作为二进制来存储到字典树中,只支持int型。
*/
template<const unsigned int ALPHABET_SIZE>
class Trie {
public:
Trie(char start) : start_(start), root_(0) {
trie_.resize(1);
}
/*
将int型变量以二进制形式插入到字典树中。
*/
void insert(int x) {
int cur = root_;
for (int i = 30; i >= 0; --i) {
int p = (x >> i) & 1;
if (trie_[cur].next_[p]== 0) {
trie_[cur].next_[p] = (int)trie_.size();
trie_.emplace_back();
}
cur = trie_[cur].next_[p];
trie_[cur].pass_count_ ++;
}
trie_[cur].end_count_ ++;
}
/*
从字典树中移除二进制形式的int型变量值。
*/
void erase(int x) {
int cur = root_;
for (int i = 30; i >= 0; --i) {
int p = (x >> i) & 1;
cur = trie_[cur].next_[p];
assert(trie_[cur].pass_count_ > 0);
trie_[cur].pass_count_ --;
}
assert(trie_[cur].end_count_ > 0);
trie_[cur].end_count_ --;
}
/*
获取字典树中异或最大值。
*/
int getMaxXor(int x) {
int cur = root_;
int res = 0;
for (int i = 30; i >= 0; --i) {
int p = (x >> i) & 1;
if (trie_[cur].next_[!p] && trie_[trie_[cur].next_[!p]].pass_count_) {
res += (1 << i);
cur = trie_[cur].next_[!p];
}
else if (trie_[cur].next_[p] && trie_[trie_[cur].next_[p]].pass_count_) {
cur = trie_[cur].next_[p];
}
else {
break;
}
}
return res;
}
/*
字典树中插入字符串。
*/
void insert(const std::string& s) {
int cur = root_;
for (const auto& x : s) {
int p = x - start_;
if (trie_[cur].next_[p] == 0) {
trie_[cur].next_[p] = (int)trie_.size();
trie_.emplace_back();
}
cur = trie_[cur].next_[p];
trie_[cur].pass_count_ ++;
}
assert(trie_[cur].end_count_ > 0);
trie_[cur].end_count_ ++;
}
/*
从字典树中移除字符串。
*/
void erase(const std::string& s) {
int cur = root_;
for (const auto& x : s) {
int p = x - start_;
cur = trie_[cur].next_[p];
assert(trie_[cur].pass_count_ > 0);
trie_[cur].pass_count_ --;
}
assert(trie_[cur].end_count_ > 0);
trie_[cur].end_count_ --;
}
/*
统计字典树中s的数量。
*/
template<typename T>
int countUnique(const T& s) {
int cur = getLastPointer(s);
return cur == 0 ? cur : trie_[cur].end_count_;
}
/*
统计字典树中前缀包含s的数量。
*/
template<typename T>
int countPrefix(const T& s) {
int cur = getLastPointer(s);
return cur == 0 ? cur : trie_[cur].pass_count_;
}
private:
char start_;
int root_;
std::vector<TrieNode<ALPHABET_SIZE>> trie_;
inline int getLastPointer(const int& x) {
int cur = root_;
for (int i = 30; i >= 0; --i) {
int p = (x >> i) & 1;
if (trie_[cur][p] == 0) {
return 0;
}
cur = trie_[cur][p];
}
return cur;
}
inline int getLastPointer(const std::string& s) {
int cur = root_;
for (const auto& x : s) {
int p = x - start_;
if (trie_[cur][p] == 0) {
return 0;
}
cur = trie_[cur][p];
}
return cur;
}
};
using namespace std;
void preProcess() {
}
void solve() {
int n, m;
cin >> n >> m;
vector<vector<pair<int, int>>> al(n + 1);
for (int i = 1; i < n; ++i){
int u, v, w;
cin >> u >> v >> w;
al[u].emplace_back(v, w);
al[v].emplace_back(u, w);
}
vector<Trie<2>> trie(2, 0);
vector<int> parity(n + 1);
vector<int> weights(n + 1);
function<void(int, int)> dfs = [&](int u, int p){
for (const auto&[v, w] : al[u]){
if (v != p){
parity[v] = parity[u] ^ 1;
weights[v] = weights[u] ^ w;
dfs(v, u);
}
}
};
dfs(1, 0);
for (int i = 1; i <= n; ++i){
trie[parity[i]].insert(weights[i]);
}
int modify = 0;
while (m --){
char t;
cin >> t;
if (t == '^'){
int x;
cin >> x;
modify ^= x;
}
else{
int v, x;
cin >> v >> x;
trie[parity[v]].erase(weights[v]);
//查询相同奇偶性的树时,要移除自身避免干扰。但是对于另一棵树就不用考虑干扰,因为xor值逻辑上独立。
cout << max(trie[parity[v]].getMaxXor(weights[v] ^ x),
trie[parity[v] ^ 1].getMaxXor(modify ^ x ^ weights[v])) << " \n"[!m];
trie[parity[v]].insert(weights[v]);
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架