2023ICPC 亚洲区域赛南京站 The 2nd Universal Cup 题解 更新至 7 题
Preface
住院了,在医院闲得无聊自己V了一场。
这场复习赛,貌似半年前还是一年前打过一次,只过了4个题。今日来还愿了。但有些题还是不会做,真的唐。
我会在代码一些有必要的地方加上注释,签到题可能一般就不会写了.
以下是代码火车头:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <unordered_map>
#include <iomanip>
#define endl '\n'
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define rep2(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
template<typename T>
void cc(const vector<T>& tem) { for (const auto& x : tem) cout << x << ' '; cout << endl; }
template<typename T>
void cc(const T& a) { cout << a << endl; }
template<typename T1, typename T2>
void cc(const T1& a, const T2& b) { cout << a << ' ' << b << endl; }
template<typename T1, typename T2, typename T3>
void cc(const T1& a, const T2& b, const T3& c) { cout << a << ' ' << b << ' ' << c << endl; }
void cc(const string& s) { cout << s << endl; }
void fileRead() {
#ifdef LOCALL
freopen("D:\\AADVISE\\cppvscode\\CODE\\in.txt", "r", stdin);
freopen("D:\\AADVISE\\cppvscode\\CODE\\out.txt", "w", stdout);
#endif
}
void kuaidu() { ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); }
inline int max(int a, int b) { if (a < b) return b; return a; }
inline double max(double a, double b) { if (a < b) return b; return a; }
inline int min(int a, int b) { if (a < b) return a; return b; }
inline double min(double a, double b) { if (a < b) return a; return b; }
void cmax(int& a, const int& b) { if (b > a) a = b; }
void cmin(int& a, const int& b) { if (b < a) a = b; }
void cmin(double& a, const double& b) { if (b < a) a = b; }
void cmax(double& a, const double& b) { if (b > a) a = b; }
using PII = pair<int, int>;
using i128 = __int128;
using vec_int = std::vector<int>;
using vec_char = std::vector<char>;
using vec_double = std::vector<double>;
using vec_int2 = std::vector<std::vector<int>>;
using que_int = std::queue<int>;
Problem A. Cool, It’s Yesterday Four Times More
我们首先知道,在一个联通块里的袋鼠是肯定能通过某种走法只剩余自己一个袋鼠。
如果一个联通块\(A\)里的存在一只袋鼠没有办法把别的联通块\(B\)里的袋鼠全干掉,就说明这一整个联通块\(A\)的袋鼠都没有办法干掉\(B\)。
我们记录下来一个联通块里随便一只袋鼠\(a\)的走位,可以发现同一联通块里的袋鼠以及他们的走位都是可以用这个走位和袋鼠\(a\)表达出来。这也是上述的原因。
那问题是别的联通块的袋鼠我们该怎么判断。直接暴力全部扫就好了。注意时间复杂度,是允许\(n*n*m*m\)的。
设联通块有\(k\)个,大概联通块的大小是\(nm/k\),枚举别的联通块里的所有袋鼠走一遍当前联通块里的\(BFS\)得到的走位,是\(O(nm*nm/k)\),所以最后总的时间复杂度是\(O(n^2m^2)\)。
//--------------------------------------------------------------------------------
const int N = 1e3 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
char A[N][N];//地图
int vis[N][N];//记录袋鼠在第几个联通块
int id;//代表编号联通块
int Q[5] = { 0,0,1,-1 };
int W[5] = { 1,-1,0,0 };
//--------------------------------------------------------------------------------
//struct or namespace:
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n >> m;
rep(i, 1, n) rep(j, 1, m) cin >> A[i][j];
rep(i, 1, n) rep(j, 1, m) vis[i][j] = 0;
id = 0;
int ans = 0;
rep(i, 1, n) rep(j, 1, m) {
if (vis[i][j] or A[i][j] == 'O') continue;
vector<PII> path;//记录这个[i,j]袋鼠的走的路径
queue<PII> F;
F.push({ i,j });
id++;
while (!F.empty()) {
auto [x, y] = F.front(); F.pop();
if (vis[x][y]) continue;
vis[x][y] = id;
path.push_back({ x - i,y - j });
for (int k = 0; k <= 3; k++) {
int tx = x + Q[k], ty = y + W[k];
if (tx<1 or ty<1 or tx>n or ty>m) continue;
if (A[tx][ty] == 'O') continue;
F.push({ tx,ty });
}
}
// for (auto [x, y] : path) cc(x, y);
// rep(q, 1, n) {
// rep(p, 1, m) cout << vis[q][p] << " ";
// cout << endl;
// }
// cc(path.size());
rep(x1, 1, n) rep(y1, 1, m) {
if (vis[x1][y1] == vis[i][j]) continue;
if (A[x1][y1] == 'O') continue;
bool flag = 0;//代表枚举的这个x1,y1的袋鼠有没有一种死法
for (auto [dx, dy] : path) {
if (x1 + dx<1 or x1 + dx>n or y1 + dy<1 or y1 + dy>m) {
flag = 1;
break;
}
if (A[x1 + dx][y1 + dy] == 'O') {
flag = 1;
break;
}
}
//如果等于0代表死不了,则当前联通块就无法只剩余一个袋鼠
if (flag == 0) goto Z;
}
ans += path.size();
Z:;
}
cc(ans);
}
return 0;
}
/*
*/
Problem C. Primitive Root
数学式子,整理一下就好了。
\(g=(kp+1)异或(p-1)\)
由于异或最大值是两者相加,最小值是大值减小值。
\((k-1)\)p+2 <= g <= (k+1) p
由此能得出\(k\)的多少等价于\(g\)的多少。但还要记住\(g<=m\),所以如果\((k-1)*(p+2)\)大于\(m\)的话,那么\(g\)就再也没有取值了。
而以上这个\(k\)的范围是在\([m/p,(m-2)/p+1]\)之间,范围不大,可以直接枚举。
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//g=(kp+1)^(p-1) <=(k+1)*p ->(k-1)*p+2 <= g <= (k+1)*p g<=m
//--------------------------------------------------------------------------------
//struct or namespace:
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
int p; cin >> p >> m;
int ans = 0;
if (m / p - 1 >= 0) {
ans += m / p;
}
else {
ans += 0;
}
int k = m / p - 1 + 1;
while (k <= (m - 2) / p + 1) {
if (((k * p + 1) ^ (p - 1)) <= m) ans++;
k++;
}
cc(ans);
}
return 0;
}
/*
*/
Problem F. Equivalent Rewriting
原题问除了原本的排序,还有没有别的排序使得最后的结果不变的。
其实我们会发现,这只需要满足在该处最后一个赋值的\(i\)不变就好了。\(i\)是行。而在该处之前的赋值的\(i\)就无所谓顺序了。
所以就会像一个拓扑图一样。所有在该处之前赋值的的都给最后一个连一条边,然后跑一个拓扑排序就好了。
注意的一个小细节就是,我们有可能最后得到的方案是原本的排序方式。所以我们先用优先队列,使得我们能用编号大的就先用编号大的。最后需要特判一下如果是\(1-n\)的排序就说明\(no\)
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
vec_int A[N];
vec_int val[N];
int in[N];
vec_int ans;
//--------------------------------------------------------------------------------
//struct or namespace:
//--------------------------------------------------------------------------------
void dfs() {
priority_queue<int> F;
rep2(i, n, 1) {
if (in[i] != 0) continue;
F.push(i);
// cc(i);
}
while (!F.empty()) {
auto x = F.top(); F.pop();
ans.push_back(x);
for (auto y : A[x]) {
in[y]--;
if (in[y] == 0) F.push(y);
}
}
int las = 0, fl = 0;
for (auto& x : ans) {
if (x != las + 1) fl = 1;
las = x;
}
if (ans.size() == n and fl) {
cc("Yes");
cc(ans);
}
else {
cc("No");
}
}
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n >> m;
ans.clear();
rep(i, 1, n) { A[i].clear(); }
rep(i, 1, m) {
in[i] = 0;
val[i].clear();
}
rep(i, 1, n) {
int a; cin >> a;
rep(j, 1, a) {
int b; cin >> b;
val[b].push_back(i);//记录该处都有哪几行赋过值
}
}
rep(i, 1, m) {
if (val[i].empty()) continue;
int len = val[i].size();
rep(j, 0, len - 1 - 1) {
A[val[i][j]].push_back(val[i][len - 1]);//建图
in[val[i][len - 1]]++;
}
}
dfs();
}
return 0;
}
/*
*/
Problem G. Knapsack
我觉得这个题真的不算简单,但是不懂为什么过的人不少。觉得榜歪的可能性比较大。
让我第二次再做,我的笨拙的脑子还是需要想半天。
首先先思考简单的,我们最后得到的买的物品中,肯定会有我们免费得到的,还有我们花钱得到的。那么肯定我们免费得到的是其中重量前\(k\)大的。
所以我们可以先安排重量从高到低排序,然后枚举中间断开的点\(i\),即这个点\(i\)左边的代表我们免费得到的,右边代表我们正常花钱买到的。由于我们上述说的,最后得到的物品中,免费的一定是重量前\(k\)大的,所以这种办法就是剪枝掉了一些无用情况。枚举\(i\)时,那些左边的没有被免费的点,在那个状态里我们也不会后来去买它。
所以左边跑个优先队列,右边跑个倒着的背包,这样背包我们预先处理出来就好了,复杂度是\(O(logk*n+n^2)\)
//--------------------------------------------------------------------------------
const int N = 5e3 + 10;
const int M = 1e4 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int suf[N][M];
//--------------------------------------------------------------------------------
//struct or namespace:
struct node {
int ti;
int val;
bool operator<(const node& q1) const {
return q1.val < val;
}
};
node A[N];
priority_queue<int, vector<int>, greater<int>> F;
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
//cin >> T;
while (T--) {
int k;
cin >> n >> m >> k;
rep(i, 1, n) {
// cin >> ti[i] >> val[i];
cin >> A[i].ti >> A[i].val;
}
sort(A + 1, A + n + 1, [&](const node& q1, const node& q2) {
return q1.ti > q2.ti;
});
// rep(i, 1, n) {
// cc(A[i].ti, A[i].val);
// }
rep2(i, n, 1) {
rep(j, 0, m) {
if (A[i].ti <= j) suf[i][j] = max(suf[i + 1][j], suf[i + 1][j - A[i].ti] + A[i].val);
else suf[i][j] = suf[i + 1][j];
}
}
int sum = 0, ans = 0;
int res = suf[1][m];
rep(i, 1, n) {
ans += A[i].val;
F.push(A[i].val);
while (F.size() > k) {
auto val = F.top();
ans -= val;
F.pop();
}
cmax(res, ans + suf[i + 1][m]);
}
cc(res);
}
return 0;
}
/*
*/
Problem I. Counter
签到题,简单处理一下细节地方就好。
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
//struct or namespace:
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n >> m;
vector<PII> A;
rep(i, 1, m) {
int a, b; cin >> a >> b;
A.push_back({ a,b });
}
A.push_back({ 0,0 });
sort(A.begin(), A.end(), [&](const PII& q1, const PII& q2) {
return q1.first < q2.first;
});
int len = A.size();
bool flag = 1;
rep(i, 1, len - 1) {
if (A[i].second == 0) continue;
if (A[i].first - A[i].second > A[i - 1].first) continue;
if (A[i].first - A[i].second == A[i - 1].first and A[i - 1].second == 0) continue;
if (A[i - 1].second + A[i].first - A[i - 1].first == A[i].second) continue;
flag = 0;
}
if (flag) cc("Yes");
else cc("No");
}
return 0;
}
Problem L. Elevator
真心觉得这个题要比\(G\)题简单,思路也非常的贪心,怎么过的人还没有\(G\)多呢,鉴定就是榜歪了。
先假设如果只有重量\(2\),那么我们直接从高到低向下贪心就好了。那么加入重量\(1\)呢?我们直接把两个重量\(1\)的看成一个重量\(2\)的就好了。毕竟\(k\)保证是偶数,那么我们奇数重量本身就需要匹配奇数重量的。
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
//struct or namespace:
struct node {
int c;
int to;
};
//--------------------------------------------------------------------------------
//处理都是偶数重量的情况
int dfs(vector<node>& A) {
sort(A.begin(), A.end(), [&](const node& q1, const node& q2) {
return q1.to > q2.to;
});
int len = A.size();
int ans = 0;
rep(i, 0, len - 1) {
if (A[i].c <= 0 and i < len - 1) {
A[i + 1].c += A[i].c;
continue;
}
if (i == len - 1) {
ans += (A[i].c + m - 1) / m * A[i].to;
continue;
}
ans += (A[i].c + m - 1) / m * A[i].to;//运送的时候向上取整,即如果当前的物品不够,我们就拿下次的物品先垫着,下次的物品减去本次消耗的量
if (A[i].c % m) A[i + 1].c -= m - (A[i].c % m);
}
return ans;
}
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n >> m; m /= 2;
vector<node> F, A;
rep(i, 1, n) {
int c, w, f; cin >> c >> w >> f;
if (w == 2) {
A.push_back({ c,f });
continue;
}
//专门放奇数重量的物品
F.push_back({ c,f });
}
sort(F.begin(), F.end(), [&](const node& q1, const node& q2) {
return q1.to > q2.to;
});
int len = F.size();
//和上面的dfs函数很像,此处是把两个重量1的变成重量2的。
rep(i, 0, len - 1) {
if (i == len - 1) {
A.push_back({ (F[i].c + 1) / 2,F[i].to });
continue;
}
A.push_back({ (F[i].c + 1) / 2,F[i].to });
if (F[i].c % 2) F[i + 1].c -= 1;
}
cc(dfs(A));
// cc(ans);
}
return 0;
}
/*
*/
Problem M. Trapping Rain Water
经典吃屎题,吃屎人狂喜。
大力用线段树维护就好了,时间和空间都没有问题。
先把原题的式子转化一下,\(min(f_i,g_i)\)等价是\(\sum f_i+\sum g_i-max(f_i,g_i)\),后者的\(max\)就是这个数组的\(max\),所以我们只需要维护出来\(f_i\)就好了,毕竟\(g_i\)和它长得很像,维护出来一个,另一个也很好维护。
线段树,对\(preMax\)数组进行维护,这个数组是代表前缀\(max\)。如果修改了\(a_i\),变成\(a_i+k\)那么就和当前线段树的这个位置比较,如果没有超过线段树维护这个点的\(max\),就没有什么行动。否则就要向右找到最后一个小于等于\(a_i+k\)的位置(这一步我们可以用线段树上二分),把这段区间用区间赋值(懒标记)。
综上,线段树上的节点维护的信息有:\(sum\)(preMax数组的求和),\(lan\)(懒标记),\(mmin\)(代表区间最左端的preMax,用来二分查找位置的),\(siz\)(记录区间长度)
开两个线段树直接暴力开搞。只能说手是真生了,写个线段树调了好久。。。
剩下的细节在代码里了。
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
//struct or namespace:
class SEG {
#define xl x+x
#define xr x+x+1
//TODO 节点维护信息、apply函数、up函数
struct info {
int sum = 0;
int siz = 1;
int lan = 0;//懒标记
int mmin = 0;//到达节点最左端的pre_mmax
int mmax = 0;//没有用到, 当时脑子写糊涂了。以下都是模版,直接ctrl+c、v
//其实这个函数没有太大用,我是直接在板子上修改了,其实只用apply2函数也ok的。
void apply1(int k) {
sum += siz * k;
lan += k;
mmin += k;
mmax += k;
}
void apply2(int k) {
mmin = k;
mmax = k;
sum = siz * k;
lan = k;
}
friend info operator+(const info& q1, const info& q2) {
info q;
q.siz = q1.siz + q2.siz;
q.sum = q1.sum + q2.sum;
q.mmin = min(q1.mmin, q2.mmin);
q.mmax = max(q1.mmax, q2.mmax);
return q;
}
};
int L, R;
vector<info> F;
void init(int x, int l, int r) {
if (l == r) { F[x] = info(); return; }
int mid = l + r >> 1;
init(xl, l, mid), init(xr, mid + 1, r);
F[x] = F[xl] + F[xr];
}
void down(int x) { if (!F[x].lan) return; F[xl].apply2(F[x].lan), F[xr].apply2(F[x].lan); }
void add(int x, int l, int r, int l1, int r1, int k) {
if (l1 > r1) return;
if (l != r) down(x); F[x].lan = 0;
if (l1 <= l and r <= r1) { F[x].apply1(k); return; }
int mid = l + r >> 1;
if (r1 <= mid) add(xl, l, mid, l1, r1, k);
else if (mid < l1) add(xr, mid + 1, r, l1, r1, k);
else add(xl, l, mid, l1, mid, k), add(xr, mid + 1, r, mid + 1, r1, k);
F[x] = F[xl] + F[xr];
}
void add2(int x, int l, int r, int l1, int r1, int k) {
if (l1 > r1) return;
if (l != r) down(x); F[x].lan = 0;
if (l1 <= l and r <= r1) { F[x].apply2(k); return; }
int mid = l + r >> 1;
if (r1 <= mid) add2(xl, l, mid, l1, r1, k);
else if (mid < l1) add2(xr, mid + 1, r, l1, r1, k);
else add2(xl, l, mid, l1, mid, k), add2(xr, mid + 1, r, mid + 1, r1, k);
F[x] = F[xl] + F[xr];
}
//二分,找到小于等于val的最后一个位置
int erfen(int x, int l, int r, int val) {
int mid = l + r >> 1;
if (l == r) return l;
if (l != r) down(x); F[x].lan = 0;
if (F[xr].mmin <= val) return erfen(xr, mid + 1, r, val);
else return erfen(xl, l, mid, val);
}
info qry(int x, int l, int r, int l1, int r1) {
if (l1 > r1) return info();
if (l != r) down(x); F[x].lan = 0;
if (l1 <= l and r <= r1) return F[x];
int mid = l + r >> 1;
if (r1 <= mid) return qry(xl, l, mid, l1, r1);
else if (mid < l1) return qry(xr, mid + 1, r, l1, r1);
else { return qry(xl, l, mid, l1, mid) + qry(xr, mid + 1, r, mid + 1, r1); }
}
#undef xl
#undef xr
public:
//TODO 调整乘的系数
SEG(int l, int r) { L = l, R = r; F.resize(r * 2.7); init(1, l, r); }
void clear(int l, int r) { L = l, R = r; init(1, l, r); }
void add(int l, int r, int k) { add(1, L, R, l, r, k); }
void add2(int l, int r, int k) { add2(1, L, R, l, r, k); }
info qry(int l, int r) { return qry(1, L, R, l, r); }
int erfen(int val) { return erfen(1, L, R, val); }
};
SEG seg(1, N)
//开了两个线段树, 下面线段树是用来维护g数组的
SEG seg2(1, N);
int A[N], B[N], pre1[N], pre2[N];
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n;
int mmax = 0, sum = 0;
rep(i, 1, n) {
cin >> A[i];
sum += A[i];
cmax(mmax, A[i]);
}
seg.clear(1, n);
seg2.clear(1, n);
rep(i, 1, n) {
B[i] = A[n - i + 1];
pre1[i] = max(pre1[i - 1], A[i]);
pre2[i] = max(A[n - i + 1], pre2[i - 1]);
seg.add(i, i, pre1[i]);
seg2.add(i, i, pre2[i]);
// cc(pre2[i]);
}
// cc(seg.qry(1, n).sum);
cin >> m;
rep(i, 1, m) {
int a, b; cin >> a >> b;
A[a] += b;
cmax(mmax, A[a]);
sum += b;
int l, r;
int val = seg.qry(a, a).mmin;
if (A[a] > val) {
l = a, r = seg.erfen(A[a]);
if (l <= r)seg.add2(l, r, A[a]);
}
// cc(seg.qry(1, n).sum);
B[n - a + 1] += b;
// seg2.add(n - a + 1, n - a + 1, b);
val = seg2.qry(n - a + 1, n - a + 1).mmin;
if (B[n - a + 1] > val) {
l = n - a + 1, r = seg2.erfen(B[n - a + 1]);
if (l <= r) seg2.add2(l, r, B[n - a + 1]);
// if (i == 2) cc(l, r);
}
// cc(l, r);
// cc(seg2.qry(1, n).sum);
cc(seg.qry(1, n).sum + seg2.qry(1, n).sum - sum - n * mmax);
// rep(j, 1, n) {
// cc(seg2.qry(j, j).sum);
// }
}
// cc(mmax);
}
return 0;
}
/*
*/
PostScript