2024CCPC 全国邀请赛(山东)暨山东省赛 题解 更新至 10 题
2024CCPC 全国邀请赛(山东)暨山东省赛 题解 更新至 10 题
Preface
这场打崩了,前有A题签到直接写不出来,后有M区间dp自己唐完了,很典的区间dp自己叭叭的半天,结果竟然写不出来,没有反应过来可以在区间dp的时候就可以算出来面积,总是想着要找出来点集然后暴力硬算,再加上算错了时间复杂度,最后直接give up了.只能说是非常的弱智了.
我会在代码一些有必要的地方加上注释,签到题可能一般就不会写了.
所有代码前面的火车头
#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(vector<T> tem) { for (auto x : tem) cout << x << ' '; cout << endl; }
void cc(int a) { cout << a << endl; }
void cc(int a, int b) { cout << a << ' ' << b << endl; }
void cc(int a, int b, int c) { cout << a << ' ' << b << ' ' << c << 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 int min(int a, int 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; }
using PII = pair<int, int>;
using i128 = __int128;
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
Problem A. 打印机
很唐的一集啊,第一发没有注意中间可能爆\(long long\)导致WA,第二发在改的时候不知道怎么着把上界改成了\(1e9\)导致WA,但是没看出来,以为是大小还是不够的原因.
气的鼠鼠直接开启了\(int128\),一边骂着这狗题一边写,中间又因为不熟悉\(i128\)出现的各种问题,调了\(1h\)再过的.中间被队友带飞直接过了两道题.
实际上直接开\(long long\),上界调成\(2e9\)就好了.
思路就是二分时间,看每一台机器在这\(mid\)时间里最后造的数量有没有超过\(k\).
唉,一个一眼签到却调了\(1h\)的吃屎选手.
//--------------------------------------------------------------------------------
const int N = 1e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e18;
int n, m, T;
int t[N], L[N], W[N];
int k;
//--------------------------------------------------------------------------------
void out(i128 r) {
string s = "";
while (r) {
s += (r % 10) + '0';
r /= 10;
}
reverse(s.begin(), s.end());
cout << s << endl;
}
int dfs(i128 mid) {
i128 sum = 0;
rep(i, 1, n) {
i128 ll = i128(i128(W[i]) + i128(i128(t[i]) * i128(L[i])));
// out(ll);
sum += i128(i128(mid) / i128(ll) * i128(L[i]));
if (sum >= k) return 1;
i128 las = mid - ((mid / ll) * ll);
if (las >= i128(t[i]) * i128(L[i])) sum += i128(L[i]);
else {
sum += i128(las / i128(t[i]));
}
if (sum >= k) return 1;
}
if (sum >= k) return 1;
return 0;
}
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n >> k;
rep(i, 1, n) {
cin >> t[i] >> L[i] >> W[i];
}
i128 l = 0, r = 1e19 + 2;
// i128 rr = 1e20;
// out(rr);
// cc(r);
// cout << (1ll << 62) << endl;
while (l + 1 != r) {
i128 mid = (l + r) / 2;
// mid = 1e18;
if (dfs(mid)) r = mid;
else l = mid;
// break;
}
// cout << r << endl;
out(r);
}
return 0;
}
/*
*/
Problem C. 多彩的线段 2
这个题有点小搞笑了,上来第一眼直接口胡说把他转化成图上的问题,每一个线段看成一个点,然后有交集的线段就互相之间连一条边,然后相邻的点不能染一样的颜色,求所有的方案数.然后就屁都想不出来了,貌似后来搜了一下是图上的一个有点典的问题,复杂度不小.
只能说唐了,思考了之后无果,然后队友说好像是可以直接模拟做的,瞬间给我茅塞顿开.只能说签到题开的小丑了.
所以我们只需要这样按照左端点排个序,从左到右开始遍历,然后对于枚举当前的线段,找前面有几个跟它接触的,有几个就 \(k\) 减去几,这就是它自己对于答案的贡献数字,最后乘起来就好了.
稍微注意的就是为了快点查询有几个接触的,可以用个堆(里面按照右端点排序),这样最后复杂度多了一个 \(log\)
//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 998244353;
const int INF = 1e16;
int n, m, T;
PII A[N];
//--------------------------------------------------------------------------------
struct node {
int y;
bool operator<(const node& q1) const {
return q1.y < y;
}
};
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
int k;
cin >> n >> k;
rep(i, 1, n) {
int a, b; cin >> a >> b;
A[i] = { a,b };
}
int tem = k;
sort(A + 1, A + n + 1, [&](PII a, PII b) {
return a.first < b.first;
});
priority_queue<node> F;
int ans = 1;
rep(i, 1, n) {
while (!F.empty() and F.top().y < A[i].first) {
tem++;
F.pop();
}
ans *= tem--;
ans %= mod;
F.push({ A[i].second });
}
cc(ans);
}
return 0;
}
/*
*/
Problem D. 王国英雄
首先能够想到的就是我们一定是先能买多少买多少,再能卖多少卖多少,称这个为 \(one\) 一个周期,然后我们一直进行这个周期.
先说一个基本的式子:
当前拥有的钱是 \(m\),最多能买的面粉就是 \(x=m/p (向下取整)\),那么 \(one\) 之后我们的钱是 \(m+(q-p)*x\),耗费的时间是 \(t=(ax+b+cx+d)\)
但是如果我们只是这样做,时间复杂度并不会允许.因为可能 \(a,b,c,d,x\)都会很小,时间复杂度会直接卡成 \(O(t)\) 级别的,
队友张神提出了一个很好的优化,就是其实我们还能够进一步求出来能够买 \(x+1\) 需要的时间, 即设购买 \(l\) 轮之后我们可以买 \(x+1\) 个面粉了,那么就有 \(m+l*(q-p)*x>=(x+1)*p\) ,即 $l >= (p(x+1)-m)/((q-p)x) $ ,向上取整就是 \(l\) 的取值.
这样时间复杂度因为 \(x\) 每次枚举都会 $ +1$ (请联想\(1+2+...+n=t\)这个式子),所以会变成 \(O(\sqrt[2]t)\).
那么还有要考虑的就是最后一段,当我们之后不能一直买到 \(x+1\) 的面粉了,剩下的一段时间,我们直接二分处理(懒得想式子)就好了.
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
int p, a, b, q, c, d, t;
cin >> p >> a >> b >> q >> c >> d >> m >> t;
if (m < p) {
cout << m << endl;
continue;
}
int ci, t1, ll;
while (1) {
//ci是上文提到的x
ci = m / p;
//t1是买一次的时间
t1 = (a + c) * ci + b + d;
//ll是刚才说的那个能够买x+1个面粉的轮数,分子上多了一部分分母-1是为了向上取整,例如a/b,如果想要向上取整,就写(a+b-1)/b
ll = (p * (ci + 1) - m + (q - p) * ci - 1) / ((q - p) * ci);
// cmin(ll, t / (t1));
//t是我们目前还剩余的时间,如果不够就break
if (ll * t1 > t) break;
t -= ll * t1;
m += ll * (q - p) * ci;
}
//跳出来之后,记得算一下我们能够买几次x个面粉
m += t / t1 * (q - p) * ci;
t -= t / t1 * t1;
//之后二分买面粉的个数,如果二分的个数在剩余的时间能够买完再卖完就l=mid,最后l就是我们最后买的面粉.
int l = 0, r = t + 1;
while (l + 1 != r) {
int mid = l + r >> 1;
if (a * mid + b + c * mid + d <= t) l = mid;
else r = mid;
}
m += (q - p) * l;
cout << m << endl;
}
return 0;
}
/*
*/
Problem E. 传感器
非常有意思的一道题,赛时被M单防了,没有看着题,之后补题想着思路是能不能直接把传感器挂到线段树上,然后再做操作,但又觉得时间复杂度不太允许,便作罢.结果发现还真是这样做,但是要加上一些小优化.
首先先开一个线段树,线段树上的节点维护的信息有 \(sum (代表红球的个数之和)\),还有一个 $vector
这样空间是不会爆的,节点里的\(vector\)上最多会有 \(mlogm\) 个编号,但是在更新的时候想着每次少一个,如果编号都要更新\(val\)的话,最后时间复杂度会变成 \(O(nmlogm)\) .
但是实际上我们只关心 \(sum\) 是不是1而已,设当前区间长度\(len\),我们只当 $sum==1 $ 或者 \(sum==0\)的时候再更新就好了. 如果\(sum==1\), \(val\) 就减\(len-1\),这样复杂度里的 \(n\) 就会变成常数.
//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int val[N];
int ans;
//--------------------------------------------------------------------------------
//namespace or struct:
//线段树板子,里面改了add函数,又加了一个dfs函数用来维护节点上的传感器
namespace seg {
#define xl x+x
#define xr x+x+1
const int N = 5e5 + 10; const int LIM = N * (2.1);
struct node {
int sum = 1;
// vector<int> A;
};
node F[LIM];
vector<int> A[LIM];
node operator+(const node& q1, const node& q2) {
node q;
// q.A.clear();
q.sum = q1.sum + q2.sum;
return q;
}
void apply(int x, int k) {
F[x].sum += k;
}
void init(int x, int l, int r) {
A[x].clear();
if (l == r) {
F[x] = node();
//记得清空A,这里WA了两发
A[x].clear();
return;
}
int mid = l + r >> 1;
init(xl, l, mid), init(xr, mid + 1, r);
F[x] = F[xl] + F[xr];
}
void add(int x, int l, int r, int l1, int r1, int k) {
if (l1 > r1) return;
if (l1 <= l and r <= r1) {
apply(x, k);
if (F[x].sum == 0) {
//遍历当前节点的传感器,val[id]都减1,因为区间长度只能是1此时
for (auto& id : A[x]) {
val[id] -= 1;
if (val[id] == 0) ans -= id * id;
if (val[id] == 1) ans += id * id;
}
}
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];
if (F[x].sum == 1) {
for (auto& id : A[x]) {
val[id] -= r - l + 1 - 1;
if (val[id] == 1) ans += id * id;
if (val[id] == 0) ans -= id * id;
}
}
else if (F[x].sum == 0) {
for (auto& id : A[x]) {
val[id] -= 1;
if (val[id] == 1) ans += id * id;
if (val[id] == 0) ans -= id * id;
}
}
}
node qry(int x, int l, int r, int l1, int r1) {
if (l1 > r1) return node();
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); }
}
//实现上述说的节点里的vector
void dfs(int x, int l, int r, int l1, int r1, int& id) {
if (l1 <= l and r <= r1) {
A[x].push_back(id);
return;
}
int mid = (l + r) >> 1;
if (r1 <= mid) dfs(xl, l, mid, l1, r1, id);
else if (mid < l1) dfs(xr, mid + 1, r, l1, r1, id);
else {
dfs(xl, l, mid, l1, mid, id);
dfs(xr, mid + 1, r, mid + 1, r1, id);
}
}
#undef xl
#undef xr
}
//-----------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n >> m;
seg::init(1, 1, n);
rep(i, 1, m) {
int l, r; cin >> l >> r;
//小球的下标是从0开始的!!!
l += 1, r += 1;
val[i] = r - l + 1;
if (val[i] == 1) ans += i * i;
seg::dfs(1, 1, n, l, r, i);
}
cout << ans << " ";
rep(i, 1, n) {
int a; cin >> a;
a += ans; a %= n; a += 1;
// cc(a);
seg::add(1, 1, n, a, a, -1);
cout << ans << " ";
}
cout << endl;
}
return 0;
}
/*
*/
Problem F. 分割序列
首先对于这种\(i*s[i]\)的求和,(\(s[i]\)代表局部内的和,共\(k\)部分),我们可以直接直观的把他转化成是后缀和.每一部分的划分后的贡献相当于是这一部分的起点 \(l\) 一直到 \(n\) 的求和.这里可以画图理解一下,会更加形象.
所以我们只需要求出来前 \(k-1\) 大的后缀和就好了,划分 \(k\) 个部分,相当于是切了 \(k-1\) 刀对这个数列.
//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N], suf[N];
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n;
rep(i, 0, n + 1) suf[i] = 0;
rep(i, 1, n) {
cin >> A[i];
}
priority_queue<int> F;
int sum = 0;
rep2(i, n, 2) {
sum += A[i];
suf[i] = suf[i + 1] + A[i];
F.push({ suf[i] });
}
sum += A[1];
cout << sum << " ";
while (!F.empty()) {
auto val = F.top(); F.pop();
sum += val;
cout << sum << " ";
}
cout << endl;
}
return 0;
}
/*
*/
Problem H. 阻止城堡
原谅我是一个\(SB\),我竟然一开始想的只需要在十字路口的时候就放就可以了,码了半天结果被样例\(hark\)了.\(ok\),重新整理思路...
这个题写的就十分\(恶心\)了,能够半模半觉的感觉化成二分图或者往网络流那边靠,但不知道该怎么写出来,看了题解才知道怎么维护,学到了学到了.
我们可以将里面需要放障碍物的情况分成两类:一类是x轴上相邻的,一类是y轴上相邻的.但是有一种情况就是有时候一个障碍物可以同时阻挡以上两类.但不能简单的直接只要是一个十字路口就放一个障碍物,我们可以考虑以下:
我们发现实际只需要两个障碍物就可以了,放了一个障碍物之后会影响别的地方,这很二分图的感觉.
平常的二分图里面将点分成左右两部分,然后跑匈牙利.对于这道题我们可以将左右相邻的点看做左边的点.上下相邻的点看做右边的点,然后如果有十字路口那么就给这两个新点连一条边,建完图之后跑匈牙利.
//--------------------------------------------------------------------------------
const int N = 5e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
bool ff[N];
//--------------------------------------------------------------------------------
//二分图板子
namespace KM {
const int N = 5e2 + 10;
int n, m;
vector<int> A[N];//n
int co[N];//m
bool vis[N];//m
void init(int n_, int m_) {
n = n_, m = m_;
for (int i = 0; i <= max(n, m); i++) {
A[i].clear();
co[i] = -1;
}
}
void add(int x, int y) { A[x].push_back(y); }
bool dfs(int x) {
for (auto y : A[x]) {
if (vis[y]) continue; vis[y] = 1;
if (co[y] == -1 or dfs(co[y])) { co[y] = x; return 1; }
}
return 0;
}
int work() {
int cnt = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= m; j++) vis[j] = 0;
if (dfs(i)) cnt++;
}
return cnt;
}
}
struct node {
int id;
int q1;
int q2;
};
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
unordered_map<int, vector<PII>> X, Y;
cin >> n;
rep(i, 0, n) ff[i] = 0;
rep(i, 1, n) {
int a, b; cin >> a >> b;
//X轴上的点,1代表是人,0代表是障碍物
X[a].push_back({ b,1 });
Y[b].push_back({ a,1 });
}
cin >> m;
rep(i, 1, m) {
int a, b; cin >> a >> b;
X[a].push_back({ b,0 });
Y[b].push_back({ a,0 });
}
vector<node> XX, YY;
//XX是代表二分图中左边的点,YY是代表右边的点
bool flag = 1;
for (auto [x, A] : X) {
sort(A.begin(), A.end(), [&](PII a, PII b) {
return a.first < b.first;
});
PII las = { -INF,0 };
for (auto [id, fl] : A) {
//如果las.secong==0 or fl==0:代表同轴上已经有放的障碍物了,就可以直接不管了,不需要放到XX或者YY中
if (las.first == -INF || (fl == 0 or las.second == 0)) {
las.first = id, las.second = fl;
continue;
}
//判断有没有中间挨着的点,有就是说明没有方案
if (id == las.first + 1) flag = 0;
XX.push_back({ x,las.first,id });
las.first = id, las.second = fl;
}
}
for (auto [y, A] : Y) {
sort(A.begin(), A.end(), [&](PII a, PII b) {
return a.first < b.first;
});
PII las = { -INF,0 };
for (auto [id, fl] : A) {
if (las.first == -INF || (fl == 0 or las.second == 0)) {
las.first = id, las.second = fl;
continue;
}
if (id == las.first + 1) flag = 0;
YY.push_back({ y,las.first,id });
las.first = id, las.second = fl;
}
}
// for (auto [a, b, c] : XX) {
// cc(a, b, c);
// }
// for (auto [a, b, c] : YY) {
// cc(a, b, c);
// }
if (flag == 0) {
cout << -1 << endl;
continue;
}
KM::init(XX.size(), YY.size());
int n1 = XX.size(), m1 = YY.size();
rep(i, 0, n1 - 1) rep(j, 0, m1 - 1) {
auto [x, y1, y2] = XX[i];
auto [y, x1, x2] = YY[j];
if (x1 < x and x < x2 and y1 < y and y < y2) {
//满足十字路口的就加边
KM::add(i, j);
// cc(x, y);
}
}
KM::work();
using KM::co;
// rep(i, 0, m1 - 1) cc(i, co[i]);
// cc(n1);
vector<PII> ans;
rep(i, 0, m1 - 1) {
//代表在这个十字路口放一个障碍物
if (co[i] != -1) {
ff[co[i]] = 1;
ans.push_back({ XX[co[i]].id,YY[i].id });
}
}
rep(i, 0, n1 - 1) {
if (ff[i]) continue;
ans.push_back({ XX[i].id,(XX[i].q1 + XX[i].q2) / 2 });
}
rep(i, 0, m1 - 1) {
if (co[i] != -1) continue;
ans.push_back({ (YY[i].q1 + YY[i].q2) / 2, YY[i].id });
}
cout << ans.size() << endl;
for (auto [a, b] : ans) {
cout << a << " " << b << endl;
}
}
return 0;
}
/*
*/
Problem I. 左移
一眼纯纯签到,只需要把字符串复制一遍,然后 \(for\) 循环 \(i\) 扫一遍 $ s[i] $和 \(s[i+len]\) (即字符串的头和尾)一不一样就好了.
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
string s; cin >> s;
if (s.size() == 1 || (s[0] == s[s.size() - 1])) {
cout << 0 << endl;
continue;
}
int len = s.size();
s = s + s;
bool fl = 0;
rep(i, 0, len - 1) {
if (s[i] == s[i + len - 1]) {
cout << i << endl;
fl = 1;
break;
}
}
if (!fl) cout << -1 << endl;
}
return 0;
}
Problem J. 多彩的生成树
一眼最小生成树,但是需要一些详细的分类讨论.
先从小到大排序边权,然后合并.
并查集里面有两个元素,一个是\(fa\),一个是\(fl\).//分别代表并查集内部的\(fa\)和当前并查集内部有没有合并过.
如果合并的\(fa\)一样,但是\(fl=0\)(代表没有合并过),那就是联通块的内部合并.
如果\(fl==1\),就可以直接\(return\)了.
以下都是\(fa\)不一样的情况,如果\(x\)和\(y\)的\(fl\)都是\(1\),那么两个联通块之间联一条边就好;
如果有一个是\(1\),那么\(ans\)就加\(没有内部联通的联通块大小*当前的边权\).
如果都是0,那么就是加\((联通块大小的和-1)*当前的边权\).
int ans = 0;
int val;
namespace DSU {
const int N = 1e3 + 10;
int A[N];
struct Info {
int fa;
int siz;
int fl;
};
Info dsu[N];
void init(int n) {
//TO DO 记得初始化
rep(i, 0, n) {
dsu[i].fa = i, dsu[i].siz = 1;
dsu[i].fl = 0;
}
}
int find(int x) { if (x == dsu[x].fa) return x; return dsu[x].fa = find(dsu[x].fa); }
void merge(int x, int y) {
x = find(x), y = find(y);
if (x == y and dsu[x].fl) return;
if (x == y) {
ans += (A[x] - 1) * val;
dsu[x].fl = 1;
return;
}
if (dsu[x].fl == 1 and dsu[y].fl == 1) {
ans += val;
dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
dsu[x].fl = 1;
return;
}
if (dsu[x].fl == 0 and dsu[y].fl == 0) {
ans += (A[x] + A[y] - 1) * val;
dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
dsu[x].fl = 1;
return;
}
if (dsu[x].fl == 0) {
ans += (A[x] * val);
dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
dsu[x].fl = 1;
return;
}
if (dsu[y].fl == 0) {
ans += A[y] * val;
dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
dsu[x].fl = 1;
return;
}
}
bool same(int x, int y) {
x = find(x), y = find(y);
if (x == y) return 1; return 0;
}
int size(int x) { return dsu[find(x)].siz; }
}
using DSU::dsu;
using DSU::A;
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
struct node {
int x;
int y;
int val;
};
vector<node> ed;
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n;
DSU::init(n);
ans = 0;
ed.clear();
rep(i, 1, n) {
cin >> A[i];
}
rep(i, 1, n) {
rep(j, 1, n) {
int a; cin >> a;
ed.push_back({ i,j, a });
}
}
sort(ed.begin(), ed.end(), [&](node& q1, node& q2) {
return q1.val < q2.val;
});
for (auto [x, y, val_] : ed) {
// cc(x, y, val_);
val = val_;
DSU::merge(x, y);
}
cout << ans << endl;
}
return 0;
}
/*
*/
Problem K. 矩阵
这个就没什么特别好讲解的了,一共 \(2n\) 个数字,并且只有一个子矩阵是四个角都互不一样的.
小小构造题,做法有很多.这里直接说一种做法,貌似也是题解的做法.
直接让前 \(n-2\) 行从上到下为1,2,3,...,然后最后倒数两行从左往右是n-1,n,...一直到剩下最后两列,目前填了 \(2n-4\)个了,然后把剩下四个没有填的数字放进去就好了.
//--------------------------------------------------------------------------------
const int N = 5e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N][N];
//--------------------------------------------------------------------------------
signed main() {
fileRead();
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n;
int cnt = 0;
rep(i, 1, n - 2) {
cnt++;
rep(j, 1, n) A[i][j] = cnt;
}
rep(j, 1, n - 2) {
cnt++;
rep(i, n - 1, n) {
A[i][j] = cnt;
}
}
A[n][n] = ++cnt, A[n - 1][n] = ++cnt;
A[n - 1][n - 1] = ++cnt, A[n][n - 1] = ++cnt;
cout << "Yes" << endl;
rep(i, 1, n) {
rep(j, 1, n) {
cout << A[i][j] << " ";
}
cout << endl;
}
}
return 0;
}
/*
*/
Problem L. 路径的交
前置知识:每一次修改一条边权 动态维护树的直径
这个题没有补,实在是补不动了.但是大体思路是差不多的.就是最终的路径如果能被选择,他的两段一定都是要大于等于\(k_i\)的.所以我们如果在原本的树上从叶子节点开始都去掉\(k_i\)个点之后,剩余的新树我们跑一下直径就好了.
以上是\(k\)固定的情况,那\(k\)不固定的时候,我们可以离线处理答案,使\(k\)从小到大,那么树就是从外层开始一层一层被删去的,(删边权可以使得边权为\(0\)),删去后再修改一条边权再求树的直径,转化成板子题了.为了这个题专门去补了其前置知识.
不贴代码有点难受,贴一个动态维护树直径的板子吧...
//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int w;
struct node {
int x;
int y;
int val;
};
vector<node> ed;
vector<PII> A[N];
int dis[N], L[N], R[N], dep[N];
int tot;
int O[N];
//--------------------------------------------------------------------------------
//namespace :
namespace seg {
#define xl x+x
#define xr x+x+1
//TODO 调整N的大小
const int N = 2e5 + 10; const int LIM = N * (2.7);
struct node {
int ans = 0;
int mmax = 0;
int mmin = 0;
int rm = 0;
int lm = 0;
int lan = 0;
};
node F[LIM];
//TODO up函数
node operator+(const node& q1, const node& q2) {
node q;
q.lan = 0;
q.ans = max({ q1.ans,q2.ans,q1.mmax + q2.lm,q1.rm + q2.mmax });
q.mmax = max(q1.mmax, q2.mmax);
q.mmin = min(q1.mmin, q2.mmin);
q.lm = max({ q1.lm,q2.lm,q2.mmax - 2 * q1.mmin });
q.rm = max({ q1.rm,q2.rm,q1.mmax - 2 * q2.mmin });
return q;
}
//TODO apply函数
void apply(int x, int k) {
F[x].lm -= k;
F[x].rm -= k;
F[x].mmax += k;
F[x].mmin += k;
F[x].lan += k;
}
void init(int x, int l, int r) {
if (l == r) { F[x] = node(); 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; apply(xl, F[x].lan), apply(xr, 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) { apply(x, 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];
}
node qry(int x, int l, int r, int l1, int r1) {
if (l1 > r1) return node();
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
}
//------------------------------------
void dfs(int x, int pa) {
dep[x] = dep[pa] + 1;
L[x] = ++tot;
O[tot] = x;
for (auto [y, val] : A[x]) {
if (y == pa) continue;
dis[y] = dis[x] + val;
dfs(y, x);
O[++tot] = x;
}
R[x] = tot;
}
signed main() {
fileRead();
kuaidu();
T = 1;
//cin >> T;
while (T--) {
int q; cin >> n >> q >> w;
rep(i, 1, n - 1) {
int a, b, c; cin >> a >> b >> c;
ed.push_back({ a,b,c });
A[a].push_back({ b,c });
A[b].push_back({ a,c });
}
dfs(1, 0);
seg::init(1, 1, tot);
rep(i, 1, tot) {
seg::add(1, 1, tot, i, i, dis[O[i]]);
}
int las = 0;
rep(i, 1, q) {
int d, e;
cin >> d >> e;
int dd = (d + las) % (n - 1);
int ee = (e + las) % w;
int t;
if (dep[ed[dd].x] > dep[ed[dd].y]) t = ed[dd].x;
else t = ed[dd].y;
// if (i == 3) cc(dd, t, ee - ed[dd].val);
seg::add(1, 1, tot, L[t], R[t], ee - ed[dd].val);
las = seg::qry(1, 1, tot, 1, tot).ans;
cout << las << endl;
ed[dd].val = ee;
}
}
return 0;
}
/*
*/
Problem M. 回文多边形
非常吃屎的一集,看到题手玩一下,发现很像区间\(dp\),但是又觉得不太能\(ok\),鉴定是区间\(dp\)做少了导致的,回去恶补区间地痞.中间想的时候也想到了这个题里面的好几个\(trick\),后面看题解的时候一眼就会了,想死.
假设一个数组是(点代表别的数字)
我们如果要从两端往中间做选择,逐渐选到最后,形成一个回文的序.
\(trick1:\)我们不会在不选择第一个\(3\)和最后一个\(3\)的情况去选择里面别的\(3\),如果有这种情况,我们肯定还要在把最外层的\(3\)一对也都选上才对,这样的面积才是最大的.
\(trick2:\)我们\(dp\)的下标\(l,r\)定义成选择了\(l,r\)两个点的最终值,有便于方程转移和计算.假设当前的两段是\(l,r\),找到的下一个两段是\(l_1,r_1\),那么我们完全可以计算出这之间的面积.把它拆成两个三角形,利用向量计算就好了.这样在转移的时候,我们可以用以上方式算出来,而不是暴力出点集再算面积
\(trick3:\)我们在\(trick1\)的基础上,要注意,我们可以选择最左边的\(3\)和中间的一个\(3\)作为\(l_1和r_1\),这样算出来的面积也是有可能是最大值.
还有时间复杂度的问题,递归的复杂度看不出来可以看转化成递推看,\(l,r\)的枚举是\(O(n^2)\),还有端点和中间的匹配还会有一个\(n\),所以最后是\(O(n^3)\)
\(hh\)最傻的地方是我后来按照这种想法想冲一发,但是dfs写错了导致时间复杂度搞错了最后直接开摆.
所以\(dp\)转移主要围绕上面几点来,剩下的详看代码:
//离散化板子
struct LISAN {
vector<int> F;
void init(int A[], int n) {
vector<int>().swap(F);
rep(i, 1, n) F.push_back(A[i]);
sort(F.begin(), F.end());
F.erase(unique(F.begin(), F.end()), F.end());
}
void init(vector<int> A) {
for (auto x : A) F.push_back(x);
sort(F.begin(), F.end());
F.erase(unique(F.begin(), F.end()), F.end());
}
//找到第一个大于等于val
int findda(int val) { int x = lower_bound(F.begin(), F.end(), val) - F.begin() + 1; return x; }
//找到最后一个小于等于val
int findxi(int val) { int x = upper_bound(F.begin(), F.end(), val) - F.begin(); return x; }
void change(int A[], int n) {
rep(i, 1, n) A[i] = findda(A[i]);
}
};
struct POINT {
int x;
int y;
};
//计算面积
int cal(POINT a, POINT b, POINT c) {
int tem = 0;
tem = (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
if (tem < 0) tem *= -1;
return tem;
}
//--------------------------------------------------------------------------------
const int N = 1e3 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int dp[N][N];
int A[N];
//zuo[i][j]数组代表从i的下标往左,最近的权值=j的下标,you数组同理
int zuo[N][N], you[N][N];
LISAN ds;
POINT pos[N];
//--------------------------------------------------------------------------------
int dfs(int l, int r) {
// cc(12312321);
if (dp[l][r] != -1) return dp[l][r];
if (l >= r) return dp[l][r] = 0;
if (A[l] != A[r]) return dp[l][r] = 0;
if (l + 1 == r) return dp[l][r] = 0;
// cc(l, r);
int tem = 0;
//枚举i,找到距离r-1左边最近的一样的值当我们新的端点
rep(i, l + 1, r) {
int j = zuo[r - 1][A[i]];
if (i > j) continue;
cmax(tem, dfs(i, j) + cal(pos[l], pos[r], pos[i]) + cal(pos[i], pos[j], pos[r]));
}
//与上面同理,枚举j,找到距离l+1右边最近的一样的值当新的端点
//这样的遍历方式使得我们不会有上述的选择中间的3而没有选择最两端的3
rep2(j, r - 1, l) {
int i = you[l + 1][A[j]];
if (i > j) continue;
cmax(tem, dfs(i, j) + cal(pos[l], pos[r], pos[i]) + cal(pos[i], pos[j], pos[r]));
}
// cc(tem);
dp[l][r] = tem;
return dp[l][r];
}
signed main() {
fileRead();
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n;
rep(i, 1, n + n) rep(j, 1, n + n) {
dp[i][j] = -1;
zuo[i][j] = 0;
you[i][j] = 0;
}
rep(i, 1, n) {
cin >> A[i];
A[n + i] = A[i];
}
rep(i, 1, n) {
int a, b; cin >> a >> b;
pos[i] = { a,b };
pos[i + n] = { a,b };
}
int mmax = 0;
//离散化,因为点数不多,但是权值很大.
ds.init(A, n + n);
ds.change(A, n + n);
rep(i, 1, n) cmax(mmax, A[i]);
// cc(mmax);
//预处理zuo数组
rep(i, 1, n + n) {
rep(j, 1, mmax) {
zuo[i][j] = zuo[i - 1][j];
if (A[i] == j) zuo[i][j] = i;
}
}
//预处理you数组
rep2(i, n + n, 1) {
rep(j, 1, mmax) {
you[i][j] = you[i + 1][j];
if (A[i] == j) you[i][j] = i;
}
}
// rep(i, 1, n + n) cout << A[i] << endl;
//计算ans
int ans = 0;
// ans = dfs(1, 3);
rep(i, 1, n + n) {
rep(j, i + 1, n + n) {
if (j - i + 1 > n) break;
cmax(ans, dfs(i, j));
}
}
cout << ans << endl;
}
return 0;
}
/*
*/
PostScript
这场打得稀巴烂,前期吃屎后期坐牢,评价是要狠狠的训.