2024CCPC 全国邀请赛(山东)暨山东省赛 题解 更新至 10 题
- 2024CCPC 全国邀请赛(山东)暨山东省赛 题解 更新至 10 题
- Preface
- 所有代码前面的火车头
- Problem A. 打印机
- Problem C. 多彩的线段 2
- Problem D. 王国英雄
- Problem E. 传感器
- Problem F. 分割序列
- Problem H. 阻止城堡
- Problem I. 左移
- Problem J. 多彩的生成树
- Problem K. 矩阵
- Problem L. 路径的交
- Problem M. 回文多边形
- PostScript
- Preface
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 LOCALLfreopen("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是上文提到的xci = m / p;//t1是买一次的时间t1 = (a + c) * ci + b + d;//ll是刚才说的那个能够买x+1个面粉的轮数,分子上多了一部分分母-1是为了向上取整,例如a/b,如果想要向上取整,就写(a+b-1)/bll = (p * (ci + 1) - m + (q - p) * ci - 1) / ((q - p) * ci);// cmin(ll, t / (t1));//t是我们目前还剩余的时间,如果不够就breakif (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+1const 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); }}//实现上述说的节点里的vectorvoid 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];//nint co[N];//mbool vis[N];//mvoid 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());}//找到第一个大于等于valint findda(int val) { int x = lower_bound(F。begin(), F。end(), val) - F。begin() + 1; return x; }//找到最后一个小于等于valint 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而没有选择最两端的3rep2(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;//计算ansint 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
这场打得稀巴烂,前期吃屎后期坐牢,评价是要狠狠的训。