哈尔滨之旅体验很棒, 本次CCPC哈尔滨是我队第一次参加(i/c)cpc,为纪念此行,回来补了4题,发一篇9题 题解。
可以在UCUP提交:2023CCPC哈尔滨站
不知何时能做cf上放送,如果这份题解对您有帮助,求个赞。
B. Memory
题意 : 给一个数组a,求每一个Mood(i)的正负性,Mood(i)的定义如图
题解 :是签到题,注意有递推式:Mood(i)=Mood(i-1)/2 + a[i],猜都能猜到你要是暴力算肯定是精度就消失了,准WA,但是这个/2就启发我们可以往二进制的方向考虑,随之就可以发现每次+a[i]对小数位的影响是直接刷新,而没有对小数位有运算(我们只为了判断正负号),所以就开两个变量x表示整数部分,Dec表示小数部分,用递推式模拟即可,复杂度O(n)。
代码 :
int main() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; i++) {
std::cin >> a[i];
}
std::string res;
int x = 0, Dec = 0;
// 整数位 x 和 小数位符号 Dec
for (int i = 0; i < n; i++) {
x += a[i];
if (x != 0)
res.push_back(x > 0 ? '+' : '-');
else {
if (Dec != 0)
res.push_back(Dec > 0 ? '+' : '-');
else
res.push_back('0');
}
if (std::abs(x) & 1)
Dec = (x > 0 ? 1 : -1);
x /= 2;
}
std::cout << res;
}
C. Karshilov's Matching Problem II
题意 :
1<= n,m <= 150000, 0<= w_i <= 10^8
题解:首先我们思考一个暴力,预处理出w数组的前缀和Sw,然后用z函数预处理出T的每个后缀跟S的lcp,记为z[i]好了,那么一次询问的答案就是, 直接暴力肯定会TLE,我们考虑把min函数拆开,分开计算,或许能优化一波。
于是我们尝试对于每个询问, 找到最左的mid, 满足使得 [mid, r]就是S的一个前缀,那么这样的话,大于等于mid的 i 在上述的min函数里就都取(r-i+1), 小于mid的 i 在上述的min函数里就都取z[i],这两个方向基于预处理, 都可以O(1)回答,其中小于mid的部分很好做,就用Sw数组在z[]上走一个前缀和即可;其中大于等于mid的部分不太好做,笔者选择结合KMP来预处理,每次只考虑以 i 为右端点的子串的贡献,最后再来个前缀和即可。 至于找mid的方法,笔者选择的是线段树上二分。复杂度O((n+m)logn),题解说此题还有O(n*sqrt(n))的莫队做法,但是笔者想了两天也没想出来,球球会的hxd在评论区教教。
代码 :
using i64 = long long;
// Z函数
auto Zalgo(const std::string &s) {
int n = s.size();
std::vector<int> z(n);
for (int i = 1, l = 0, r = 0; i < n; i++) {
if (i < r)
z[i] = std::min(z[i - l], r - i);
while (i + z[i] < n and s[i + z[i]] == s[z[i]])
z[i]++;
if (i + z[i] > r)
l = i, r = i + z[i];
}
return z;
}
constexpr int N = 1.5e5 + 20;
int t[N << 2];
// 线段树维护区间最大值
void up(int i) {
t[i] = std::max(t[i << 1], t[i << 1 | 1]);
}
void modify(int i, int l, int r, int x, int y) {
if (l == r) {
t[i] = y;
return;
}
int mid = l + r >> 1;
if (x <= mid)
modify(i << 1, l, mid, x, y);
else
modify(i << 1 | 1, mid + 1, r, x, y);
up(i);
}
// 线段树上二分找到 [tl, tr] 里最左边 ≥y 的下标
int res = 0;
void get(int i, int l, int r, int tl, int tr, int y) {
if (res < tr + 1 or t[i] < y)
return;
int mid = l + r >> 1;
if (tl <= l and r <= tr) {
if (l == r) {
res = l;
return;
}
get(i << 1, l, mid, tl, tr, y);
get(i << 1 | 1, mid + 1, r, tl, tr, y);
return;
}
if (tl <= mid)
get(i << 1, l, mid, tl, tr, y);
if (mid < tr)
get(i << 1 | 1, mid + 1, r, tl, tr, y);
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::string S, T;
std::cin >> S >> T;
std::vector<int> w(n + 1);
std::vector<i64> sw(n + 1);
for (int i = 1; i <= n; i++) {
std::cin >> w[i];
sw[i] = sw[i - 1] + w[i];
}
auto Z0 = Zalgo(S + "0" + T);
std::vector<int> zt(Z0.begin() + n + 1, Z0.end());
for (int i = 0; i < n; i++) {
modify(1, 0, n - 1, i, zt[i] + i - 1);
// 维护从 T[i] 开始匹配 pre(S) 最远匹配到的下标
}
auto Find = [&](int l, int r, int y) {
// 找到区间[l, r]里匹配右端点 ≥y 的最左的下标
res = r + 1;
get(1, 0, n - 1, l, r, y);
return res;
};
std::vector<int> link(n, 0);
std::vector<i64> pre(n + 1, 0);
for (int i = 0; i <= n; i++)
pre[i] = w[i]; // 首先前缀本身需要算进答案
// KMP 求pre[i] : f(pre[i])
for (int i = 1, j = 0; i < n; i++) {
while (j and S[i] != S[j])
j = link[j - 1];
j += (S[i] == S[j]);
link[i] = j;
pre[i + 1] += pre[j];
}
for (int i = 1; i <= n; i++)
pre[i] += pre[i - 1];
std::vector<i64> L(n + 1, 0); // 左段的贡献(前缀和)
for (int i = 1; i <= n; i++)
L[i] = L[i - 1] + sw[zt[i - 1]];
int l, r;
while (m--) {
std::cin >> l >> r;
--l, --r;
int mid = Find(l, r, r);
i64 ans = L[mid] - L[l] + +pre[r + 1 - mid];
std::cout << ans << '\n';
}
}
D. A Simple MST Problem
题意:定义 为x的质因子集合的大小,比如w(9)=1。每次询问给一个l,r 然后需要求出如果一个图只有l,l+1, ...r 这些点,点x和点y之间的边权是w(lcm(x,y)), 求最小生成树边权和。
题解:大家都知道 w(lcm(x, y)) = w(x) + w(y) - w(gcd(x, y))
首先考虑如果区间里有w(x) = 1的数,即x = p^k, 那么我们每个点 y 都跟 x 连边,这样的边权和就蛮小的,因为每条边最多w(y)+1,所以我们思考一个点 y 需要和谁连边的时候,只需要考虑w(y)和w(y)+1的边权即可,w(y)+1的非常好办,只需要跟w(x) = 1的 x 连边即可,w(y) 的边权需要 y 和质因子集合是 y 的子集的数 u 连边,可是这样的 u 有好多个,不仅枚举不来,就算连完了,跑mst都肯定TLE了,怎么办? 那么我们就只尝试连接离 y 最近的 u 就行,所以我们预处理出两个东西
1,距离 y 最近的小于 y 的 u,满足 u 的质因子集合是 y 的质因子集合的子集。
2,距离 y 最近的大于 y 的 u,满足 u 的质因子集合是 y 的质因子集合的子集。
然后我们每个数 y 都和左右边的 u 各连一条边权为 w(y) 的边,再和一个固定的 x 连一条边(注意 x 满足w(x) = 1),共3*n条边。最后Kruskal求mst即可, 总时间复杂度O(nlogn) 。
考虑如果区间里没有w(x) = 1的数,区间肯定很小,暴力即可。
但是笔者的做法并不优,所以需要卡常,比如求上述两个 u 的时候需要用到一个函数proP(x)表示x的质因子集合的元素乘积,这里w(x)和proP(x)都是积性函数, 可以线性筛,这里的mst用Kruskal求的时候,因为边权数值很小,可以考虑计数排序, 这样单次询问的复杂度就降到了O(r-l+1)。
代码 :
// 简单的一个并查集类
struct DSU {
std::vector<int> f;
std::vector<int> size;
DSU(int n) : f(n), size(n) {
std::iota(f.begin(), f.end(), 0);
std::fill(size.begin(), size.end(), 1);
}
int find(int x) { // 路径压缩
while (x != f[x])
x = f[x] = f[f[x]];
return x;
}
void Union(int x, int y) {
if (find(x) == find(y))
return;
if (size[x] < size[y]) // 按秩合并
std::swap(x, y);
size[find(x)] += size[find(y)];
f[find(y)] = find(x);
}
};
constexpr int N = 1e6 + 2;
int P[N], w[N], proP[N], v[N], tot;
// proP[x]是 x 的所有质因子的乘积
int L[N], R[N], cnt[N];
int CappsPre = [] {
v[1] = 1;
std::fill(proP, proP + N, 1);
// 为了卡常, 最好是把 w 和 proP 都用线性筛求
// 毕竟这里的 w 和 proP 函数都是积性函数
for (int i = 2; i < N; i++) {
if (v[i] == 0) {
P[++tot] = i;
v[i] = i;
w[i] = 1;
proP[i] = i;
}
for (int j = 1; j <= tot and i * P[j] < N; j++) {
v[i * P[j]] = P[j];
if (i % P[j] == 0) {
w[i * P[j]] = w[i];
proP[i * P[j]] = proP[i];
break;
}
w[i * P[j]] = w[i] + 1;
proP[i * P[j]] = proP[i] * P[j];
}
}
// 得到 x 的所有因子的集合
// 比O(nln(n))预处理快
auto findDiv = [&](int x) {
std::vector<int> res(1, 1);
while (x > 1) {
int d = v[x];
x /= d;
for (int i = res.size() - 1; i >= 0; i--)
res.push_back(res[i] * d);
}
return res;
};
for (int i = 1; i < N; i++) {
auto Div = findDiv(proP[i]);
for (int d : Div) {
L[i] = std::max(L[i], cnt[d]);
}
cnt[proP[i]] = i;
}
std::memset(cnt, 127, sizeof cnt);
std::memset(R, 127, sizeof R);
for (int i = N - 1; i; i--) {
auto Div = findDiv(proP[i]);
for (int d : Div) {
R[i] = std::min(R[i], cnt[d]);
}
cnt[proP[i]] = i;
}
return 0;
}();
// 计数排序, 因为 w 很小, 所以计数排序比std::sort()快
void cpSort(std::vector<std::array<int, 3>> &a) {
int M = 0; // 计数上界
for (const auto &[w, x, y] : a)
M = std::max(M, w);
std::vector<std::vector<int>> Ton(M + 1);
for (int i = 0; i < a.size(); i++) {
Ton[a[i][0]].push_back(i);
}
std::vector<std::array<int, 3>> b;
for (int x = 0; x <= M; x++) {
for (auto i : Ton[x])
b.push_back(a[i]);
}
a = b;
}
// 最小生成树函数
int mst(std::vector<std::array<int, 3>> &e, int l, int r) {
DSU dsu(r - l + 1);
cpSort(e);
int ans = 0;
for (auto [w, x, y] : e) {
if (dsu.find(x - l) == dsu.find(y - l))
continue;
ans += w;
dsu.Union(x - l, y - l);
}
return ans;
}
int edge(int x, int y) {
return w[x] + w[y] - w[std::gcd(x, y)];
}
void solve() {
int l, r;
std::cin >> l >> r;
int ans = std::accumulate(w + l, w + r + 1, 0);
if (l == 1) {
std::cout << ans << '\n';
return;
}
std::vector<std::array<int, 3>> e; // Kruskal的边集合
int primePos = 0;
for (int x = l; x <= r; x++) {
if (w[x] == 1)
primePos = x;
}
if (primePos == 0) {
// [l, r]区间里一个w[x]=1的数都没有, 直接暴力!
for (int i = l; i < r; i++) {
for (int j = i + 1; j <= r; j++) {
e.push_back({edge(i, j), i, j});
}
}
ans = mst(e, l, r);
std::cout << ans << '\n';
return;
}
for (int x = l; x <= r; x++) {
// 向子集数连边不必客气, 边权直接是w[x]就行
if (L[x] >= l)
e.push_back({w[x], x, L[x]});
if (R[x] <= r)
e.push_back({w[x], x, R[x]});
// 向不太熟悉的primePos连边客气点, 调用一下edge函数
if (primePos != x)
e.push_back({edge(x, primePos), x, primePos});
}
ans = mst(e, l, r);
std::cout << ans << '\n';
}
E. Revenge on My Boss
题意 : 给一个n长度的三元组序列 ,你需要重排这个序列,满足
最小,求一个排列方案。
题解 :这是一个最小化最大值的问题,首先尝试二分,注意并不是所有最小化最大值的问题都能二分做,但是首先往这个方向尝试是可以的。
二分出一个结果 P ,那么 P 可行 等价于 存在一个重排方案使得 对于任意的 m 都满足 suma[1, m]+sumb[m, n] <= P/c[m] ,定义d[i] = a[i] - b[i] ,sumd[i] 是d的前缀和,B=sumb[1, n], 然后就可以转化为 sumd[m-1] <= P/c[m] + B - a[m] ,正如官方题解所言:不等式右边是与排列顺序无关的量,左边是与排列顺序有关量,定义不等式右边为 e[m],天哪这不就是那个经典的 luogu P1842 [USACO05NOV] 奶牛玩杂技 嘛,所以最贪心的方案就是按照 d[i]+e[i]从小到大排序即可, 但是这里的d[i]可能是负数,这里要单独处理,对于 d[i]<=0 的点,按照e[i] 从大到小 排序即可。
复杂度O(nlognlogV)
代码 :
using i64 = long long;
struct Node {
int a, b, c, d, id;
i64 e;
};
// sumb + sumd(m) + bm <= P/cm
// sumd(m) <= P/cm +sumb - bm
// sumd(m-1) <= P/cm +sumb - am
void solve() {
int n;
std::cin >> n;
std::vector<Node> a(n);
i64 sumb = 0;
for (int i = 0; i < n; i++) {
std::cin >> a[i].a >> a[i].b >> a[i].c;
sumb += a[i].b;
a[i].id = i + 1;
a[i].d = a[i].a - a[i].b;
}
auto calc = [&](i64 x) {
for (int i = 0; i < n; i++) {
a[i].e = sumb - a[i].a + x / a[i].c;
}
std::sort(a.begin(), a.end(), [&](const Node &lhs, const Node &rhs) {
if (lhs.d <= 0 and rhs.d <= 0)
return lhs.e > rhs.e;
if (lhs.d <= 0 or rhs.d <= 0)
return lhs.d < rhs.d;
return lhs.e + lhs.d < rhs.e + rhs.d;
});
i64 sumd = 0, Max = 0;
for (int i = 0; i < n; i++) {
sumd += a[i].d;
Max = std::max(Max, 1ll * a[i].c * (sumb + sumd + a[i].b));
}
return Max <= x;
};
i64 l = 0, r = 1e18;
// L : 存在一个排列使得 对于每个i都有 sumdi<=ei 是不可能的
// R : 存在一个排列使得 对于每个i都有 sumdi<=ei 是可能的
while (l + 1 < r) {
auto mid = l + (r - l) / 2;
if (calc(mid))
r = mid;
else
l = mid;
}
calc(r);
for (int i = 0; i < n; i++)
std::cout << a[i].id << " \n"[i + 1 == n];
}
G. The Only Way to the Destination
题意 : 在N*M的网格图中放置K条横着的墙(我不知道为什么题目的图是画成竖着的)。最后判定剩余的空格子是否构成一棵树。 注意 :墙不会交叉,保证最后的空格子构成一个连通块。
题解 : 笔者的题解和官方题解很不一样。
首先树有一个性质:n个节点伴随着n-1条边,所以我们只需要算出来最后剩余的空格子部分有几个点几条边即可。考虑对 y 方向离散化, 对 x 方向建立动态开点的线段树,从低到高不断求出最后的空格子图有几条边即可,复杂度O(Klogn)
代码 :
using i64 = long long;
// 动态开点线段树
struct Node {
int val = 0, tag = 0;
Node *l = nullptr;
Node *r = nullptr;
};
void up(Node *p) {
if (!p->l)
p->val = p->r->val;
else if (!p->r)
p->val = p->l->val;
else
p->val = p->l->val + p->r->val;
}
void down(Node *p, int l, int r) {
if (p->tag == 0)
return;
int mid = l + r >> 1;
if (!p->l)
p->l = new Node();
if (!p->r)
p->r = new Node();
int b = p->tag;
p->l->val += b * (mid - l + 1);
p->r->val += b * (r - mid);
p->l->tag += b;
p->r->tag += b;
p->tag = 0;
}
// 区间加 1
void modify(Node *&p, int l, int r, int tl, int tr) {
if (!p)
p = new Node();
if (tl <= l and r <= tr) {
p->val += r - l + 1;
p->tag++;
return;
}
down(p, l, r);
int mid = l + r >> 1;
if (tl <= mid)
modify(p->l, l, mid, tl, tr);
if (mid < tr)
modify(p->r, mid + 1, r, tl, tr);
up(p);
}
// 区间求和
int get(Node *p, int l, int r, int tl, int tr) {
if (!p)
return 0;
if (tl <= l and r <= tr) {
return p->val;
}
down(p, l, r);
int mid = l + r >> 1, ans = 0;
if (tl <= mid)
ans = get(p->l, l, mid, tl, tr);
if (mid < tr)
ans += get(p->r, mid + 1, r, tl, tr);
return ans;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m, k;
std::cin >> n >> m >> k;
std::vector<std::array<int, 3>> wall(k);
std::vector<int> b = {1, m};
for (auto &[xl, xr, y] : wall) {
std::cin >> xl >> xr >> y;
b.push_back(y);
if (y != 1)
b.push_back(y - 1);
}
std::sort(b.begin(), b.end());
b.erase(std::unique(b.begin(), b.end()), b.end());
auto find = [&](int x) {
return std::lower_bound(b.begin(), b.end(), x) - b.begin();
}; // 离散化三件套, 把 y 给离散一下
std::vector wallArr(b.size(), std::vector<std::pair<int, int>>());
for (auto [xl, xr, y] : wall) {
wallArr[find(y)].push_back({xl, xr});
}
i64 nodeCnt = 1ll * n * m, edgeCnt = 2ll * n * m - n - m;
// 点数, 边数
Node *rt = new Node(); // 开个线段树
for (int y = 0; y < b.size(); y++) {
if (!wallArr[y].empty()) {
std::sort(wallArr[y].begin(), wallArr[y].end());
for (int i = 0; i < wallArr[y].size(); i++) {
auto [l, r] = wallArr[y][i];
nodeCnt -= r - l + 1;
edgeCnt += get(rt, 1, n, l, r) - (r - l + 2);
edgeCnt -= 2 * (r - l + 1);
// 墙在下底
if (y == 0)
edgeCnt += r - l + 1;
// 墙在上顶
if (y + 1 == b.size())
edgeCnt += r - l + 1;
// 俩墙左右贴着
if (i and wallArr[y][i - 1].second + 1 == l)
edgeCnt++;
// 墙贴着左侧
if (i == 0 and l == 1)
edgeCnt++;
// 墙贴着右侧
if (i + 1 == wallArr[y].size() and r == n)
edgeCnt++;
}
}
rt = new Node(); // 开个新的线段树
for (auto [l, r] : wallArr[y]) {
modify(rt, 1, n, l, r);
}
}
std::cout << (edgeCnt + 1 == nodeCnt ? "YES" : "NO");
}
H. Energy Distribution
题意 :
给出一个 n×n 的矩阵 Wn×n, 在限制 (e[i] >= 0 ) 的基础下, 求
的最大值。
题解 :
已知 F(e1, e2, ... , en) 和限制, 求 F 的最大值,考虑拉格朗日乘数法,设 L(e1, e2, ... , en, λ) = F - λ(sum ei - 1) ,对 L 的每个变量求偏导,并且等于 0(注意消去λ),然后解出答案即可。因为题目还限制了 ei >= 0,所以我们枚举 ei = 0 的集合,然后针对其余的变量去高斯消元求解, 注意舍去负解。
代码:
using ld = double;
constexpr ld eps = 1e-9;
int sgn(const ld &a) {
if (a < -eps)
return -1;
return (a > eps);
}
std::string gauss(std::vector<std::vector<ld>> &a) { // 传入增广矩阵
int n = a.size();
int c = 0, r = 0;
for (c = 0, r = 0; c < n; c++) { // c列r行,遍历列
int tmp = r;
for (int i = r; i < n; i++) // 寻找列主元
if (sgn(a[i][c]))
tmp = i;
if (sgn(a[tmp][c]) == 0) // 当前列全为0
continue;
std::swap(a[tmp], a[r]); // 交换列主元
for (int i = n; i >= c; i--) // 倒序处理
a[r][i] /= a[r][c];
for (int i = r + 1; i < n; i++)
if (sgn(a[i][c]))
for (int j = n; j >= c; j--)
a[i][j] -= a[r][j] * a[i][c];
r++;
}
if (r < n) {
for (int i = r; i < n; i++)
if (sgn(a[i][n]))
return "NoSolution";
return "InfSolution";
}
// 解放在 a[i][n] (0<= i < n)
for (int i = n - 1; i >= 0; i--)
for (int j = i + 1; j < n; j++)
a[i][n] -= a[j][n] * a[i][j];
return "OK";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector w(n, std::vector<int>(n, 0));
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
std::cin >> w[i][j];
}
}
ld ans = 0;
for (int S = 1; S < (1 << n); S++) {
int m = __builtin_popcount(S);
if (m <= 1)
continue;
std::vector a(m, std::vector<ld>(m + 1, 0));
std::vector<int> b;
for (int T = S; T; T -= T & -T) {
b.push_back(std::__lg(T & -T));
}
for (int i = 0; i <= m; i++)
a[0][i] = 1; // 第一行是 sum ei = 1
for (int j = 1; j < m; j++)
for (int k = 0; k < m; k++) {
// 消去λ
a[j][k] = w[b[j]][b[k]] - w[b[j - 1]][b[k]];
}
auto Solution = gauss(a);
if (Solution == "OK") {
ld res = 0;
for (int i = 0; i < m; i++)
if (sgn(a[i][m]) == -1)
res = -1e9; // 舍去负解
for (int i = 0; i < m; i++) {
for (int j = i + 1; j < m; j++) {
res += a[i][m] * a[j][m] * w[b[i]][b[j]];
}
}
ans = std::max(ans, res);
}
}
std::cout << std::fixed << std::setprecision(6) << ans;
}
J. Game on a Forest
题意 :
1<= m < n <= 10^5
题解 : 我们手玩一下会发现,奇数个点的树的sg函数是1, 偶数个点的树的sg函数是2,注意0个点的树的sg函数是0。然后我们就可以暴力枚举删除点和边计算答案了,算是小小的树形dp。 复杂度O(n+m)
代码 :
struct DSU {
std::vector<int> f;
std::vector<int> size;
DSU(int n) : f(n), size(n) {
std::iota(f.begin(), f.end(), 0);
std::fill(size.begin(), size.end(), 1);
}
int find(int x) {
while (x != f[x])
x = f[x] = f[f[x]];
return x;
}
void Union(int x, int y) {
if (find(x) == find(y))
return;
size[find(x)] += size[find(y)];
f[find(y)] = find(x);
}
int blockSize(int x) {
return size[find(x)];
}
};
int sg(int x) {
if (x == 0)
return 0;
return 2 - x % 2;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
DSU dsu(n + 1); // 开个并查集
std::vector e(n + 1, std::vector<int>());
for (int u, v, i = 0; i < m; i++) {
std::cin >> u >> v;
dsu.Union(u, v);
e[u].push_back(v);
e[v].push_back(u);
}
int SG = 0;
for (int i = 1; i <= n; i++) {
if (dsu.find(i) == i)
SG ^= sg(dsu.blockSize(i));
}
int ans = 0;
std::vector<int> size(n + 1, 0);
std::function<void(int, int)> dfs = [&](int x, int fa) {
size[x] = 1;
int tmpSG = 0;
for (int y : e[x]) {
if (y == fa)
continue;
dfs(y, x);
size[x] += size[y];
tmpSG ^= sg(size[y]);
}
tmpSG ^= sg(dsu.blockSize(x) - size[x]);
// 删点
if ((SG ^ tmpSG) == 0)
ans++;
// 删边
if (fa != 0 and (SG ^ sg(size[x]) ^ sg(dsu.blockSize(x) - size[x])) == 0) {
ans++;
}
};
for (int i = 1; i <= n; i++) {
if (dsu.find(i) == i) {
SG ^= sg(dsu.blockSize(i));
dfs(i, 0);
SG ^= sg(dsu.blockSize(i));
}
}
std::cout << ans;
}
L. Palm Island
题意 : 给出两个长度为n的排列a和b,对a不断进行如下操作,需要你把a操作成b
操作1 : 把数组第一项取出,push_back到末尾。
操作2 : 把数组第二项取出,push_back到末尾。
1<= n <= 1000, 需要输出具体操作,且要求操作次数 <= n^2
题解 :
此题做法应该很多, 笔者是这样做的:
不断地通过操作2,使得a[1]右边接上a[1]这个元素右边应该接上的数字, 然后再用操作1把a[1]左边应该接上的元素旋转到第一位,然后重复上述操作n次即可。
比如样例2 :
[1, 2, 3, 4] 里根据b数组可知:1右边应该要接 3 的。
所以我们通过操作2把3旋转到第二位 → [1, 3, 4, 2]
根据b数组可知:1左边应该要接 2 的。
所以我们通过操作1把 2 旋转到第一位 → [2, 1, 3, 4]
然后接下来2后面要接1(已经接好了)
然后2左边需要是4。
所以我们通过操作1把 4 旋转到第一位 → [4, 2, 1, 3]
此时肯定已经循环同构了,最后我们把数组通过操作1旋转成b即可
代码 :
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n), b(n);
for (int i = 0; i < n; i++)
std::cin >> a[i], a[i]--;
for (int i = 0; i < n; i++)
std::cin >> b[i], b[i]--;
std::vector<int> L(n);
L[b[0]] = b.back();
for (int i = 1; i < n; i++) {
L[b[i]] = b[i - 1];
}
std::string res;
for (int _ = 0; _ < n; _++) {
int base = a[0];
// 将L[a[0]]旋转到头
auto pos = std::find(a.begin(), a.end(), L[base]);
res.insert(res.end(), pos - a.begin(), '1');
std::rotate(a.begin(), pos, a.end());
// 将刚刚的a[0]旋转到1
pos = std::find(a.begin(), a.end(), base);
res.insert(res.end(), pos - a.begin() - 1, '2');
std::rotate(a.begin() + 1, pos, a.end());
}
// 最后旋转到和答案一样
auto pos = std::find(a.begin(), a.end(), b[0]);
res.insert(res.end(), pos - a.begin(), '1');
std::rotate(a.begin(), pos, a.end());
std::cout << res << '\n';
}
M. Painter
题意 : 给n个操作
1,用字符在画布上画一个圆
2,用字符在画布上画一个矩形
3,输出画布矩形范围内的内容
只有1<= n <= 1000 ,其他输入数据均在[0, 10^9]内,操作3涉及的总面积<= 10^4
题解 :
只需要每画一个位置,暴力扫描n个操作判定有没有在这个点画画即可。复杂度O(n*10^4)
(出题人说这里的10^4可以做到10^6,会有些难度)
代码 :
(这是笔者写的用到了类的继承和多态的第一个竞赛代码)
using pii = std::pair<int, int>;
#define x first
#define y second
pii operator-(pii a, pii b) {
return {a.x - b.x, a.y - b.y};
}
i64 operator*(pii a, pii b) {
return 1ll * a.x * b.x + 1ll * a.y * b.y;
}
i64 dis2(pii a, pii b) {
auto p = a - b;
return p * p;
}
bool Mid(int a, int b, int c) {
return a <= b and b <= c;
}
struct Draw {
std::string op = "";
pii p1 = {0, 0};
char col = '.';
virtual bool Inside(pii p) = 0; // 纯虚函数
};
struct Circle : public Draw {
int r = 0;
bool Inside(pii p) { // 多态
return dis2(p1, p) <= 1ll * r * r;
}
};
struct Rectangle : public Draw {
pii p2 = {0, 0};
bool Inside(pii p) { // 多态
return Mid(p1.x, p.x, p2.x) and Mid(p1.y, p.y, p2.y);
}
};
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
int qn;
std::cin >> qn;
std::vector<Draw *> a;
for (int i = 0; i < qn; i++) {
std::string op;
std::cin >> op;
if (op == "Circle") {
Circle *p = new Circle();
p->op = op;
std::cin >> (p->p1.x) >> (p->p1.y);
std::cin >> (p->r) >> (p->col);
a.push_back(p);
} else if (op == "Rectangle") {
Rectangle *p = new Rectangle();
p->op = op;
std::cin >> (p->p1.x) >> (p->p1.y);
std::cin >> (p->p2.x) >> (p->p2.y);
std::cin >> (p->col);
a.push_back(p);
} else if (op == "Render") {
pii p1, p2;
std::cin >> p1.x >> p1.y;
std::cin >> p2.x >> p2.y;
std::vector res(p2.y - p1.y + 1, std::string(p2.x - p1.x + 1, '.'));
for (auto p : a)
for (int y = p1.y; y <= p2.y; y++)
for (int x = p1.x; x <= p2.x; x++)
if (p->Inside({x, y}))
res[y - p1.y][x - p1.x] = p->col;
// 倒着输出
for (int y = p2.y; y >= p1.y; y--) {
std::cout << res[y - p1.y] << '\n';
}
}
}
}