写在前面
人生第一次 XCPC,也是最后一次 XCPC,直接被教练禁赛了
算竞生涯里最烂的一次,之前都没这么烂,全场垫底
反正也结束了,一夜回到解放前,我能说什么呢,之前也没资格不是
2025 JXCPC
K
构造
先 31 配完,
剩下的 1 非常多,就配 21121,
剩下的 3 非常多,就配 33233,但是有个特判,223,就是剩下的 3 剩下 1 个的要放到 2 的最后才有解,且 2 要是至少两个的剩余,否则就无解
然后,实现思路就是看最优配对还有剩下就不可能配对了,无解,输出 -1
void solve(){
cin >> n;
vector<vector<int>> pos(4);
for(int i=1;i<=n;++i){
cin >> x, x%=4;
num[i] = x;
pos[x].push_back(i);
// cerr << x << ' ';
}
// for(int i=0;i<4;++i){
// for(auto x : pos[i]) cerr << x << ' ';
// cerr << '\n';
// }
// 7 + 3 + 5
// 3 3 2 3 3 2 3 3 2
// 3 1 3 1 3 1 3 1 3 1 3 3 2 2 2
vector<int> ans;
while(pos[0].size()) ans.pb(pos[0].back()), pos[0].pop_back();
while(pos[1].size() && pos[3].size()){
ans.pb(pos[3].back()), pos[3].pop_back();
ans.pb(pos[1].back()), pos[1].pop_back();
}
if(pos[1].size()){
while(pos[1].size() > 1 && pos[2].size()){
ans.pb(pos[2].back()), pos[2].pop_back();
ans.pb(pos[1].back()), pos[1].pop_back();
ans.pb(pos[1].back()), pos[1].pop_back();
}
if(pos[1].size() == 1) {
while(pos[2].back()) ans.pb(pos[2].back()), pos[2].pop_back();
}else if(pos[1].size()) return cout << -1 << '\n', void();
}else if(pos[3].size()){
while(pos[3].size() > 1 && pos[2].size()){
ans.pb(pos[3].back()), pos[3].pop_back();
ans.pb(pos[3].back()), pos[3].pop_back();
ans.pb(pos[2].back()), pos[2].pop_back();
}
if(pos[3].size() == 1){
if(pos[2].size() % 2 == 0){
while(pos[2].size()) ans.pb(pos[2].back()), pos[2].pop_back();
ans.pb(pos[3].back()), pos[3].pop_back();
}
}else if(pos[3].size() > 1){
if(pos[3].size() > 2) return cout << -1 << '\n', void();
else{
while(pos[3].size()) ans.pb(pos[3].back()), pos[3].pop_back();
}
}
}
while(pos[2].size()) ans.pb(pos[2].back()), pos[2].pop_back();
for(auto x : ans) cout << x << ' ';
cout << '\n';
}H
dp
题意:
空序列 b 生成序列 a,两种操作,
操作 1:第 i 次操作在 b 序列末尾增加数字 i
操作 2:在任意数字上 +1
生成序列的最小操作数量
思路:
硬控了一天了,结果就这?
就是限制一组第 chosei 时刻的数字,作为插入操作,一共 n 次,剩下的就是 +1 操作,每个的贡献是 ai - chosei,总共操作数是 n + sum(ai) - sum(chosei)
则,ans = max(mx,ans = n + sum(ai) - sum (chosei))
这个限制的 chosei,先考虑倒推到一组单调递增的序列作为基准
然后再考虑插入 chosei 的时间是否准确,chosei 必须在 i+preSum 之内才能加入,否则不行
如果满足以上两个条件还是无法满足单调递增,无法构造,输出 -1
int work(){
vector<int> chose(n+1);
chose[n] = num[n];
for(int i=n-1;i>=2;--i) chose[i] = min(num[i], chose[i+1]-1);
chose[1] = 1;
int now = num[1] - chose[1];
// chose[i]:第 i 次加入的时刻, 而且必须在已经操作次数 i + now, 否则无法增加
// 而且必须使用滚动变量完成
for(int i=2;i<=n;++i){
chose[i] = min(chose[i], i+now);
if(chose[i] < chose[i-1] + 1) return -1;
now += num[i] - chose[i];
}
ll ans = n + accumulate(all(num), 0ll) - accumulate(all(chose),0ll);
ans = max(ans, *max_element(all(num)));
return ans;
}
void solve(){
cin >> n, num.assign(n+1,0);
bool wa = 0;
For(i,1,n) cin >> num[i], wa |= (num[i] < i);
if(wa) return cout << -1 << '\n', void();
else cout << work() << '\n';
}C
思路:
滑窗 + 线段树
题解说线段树
思路很好出啊,知道了标签之后,就很好想思路了
就是滑窗控制选择的答案子区间范围,然后区间内的所有线段划分为不超过 $k$ 组,其实就是线段上某个点属于不超过 $k$ 个区间,
每次加入一个区间,就是这个区间内打上 $+1$ 的标记就行了,查询 $1,n$ 内的区间最大值 $>k$ 就是不合法的,这个线段跳过,否则就加入
一个线段的判断已经出了,就是怎么找到最大区间?
无法跳过,好办,直接滑窗,无法扩展右端点,直接移动左端点即可,结束
1A,不出签到到不了我的主场,改个板子半小时就调完了
template <class Info>
struct SegmentTree {
struct Node {
int l, r;
Info info;
int tag;
};
std::vector<Node> tr;
SegmentTree() {};
SegmentTree(int n) {
init(n);
}
SegmentTree(std::vector<Info> & info) {
init(info);
}
void init(int n) {
tr.assign(n << 2, {});
build(0, n - 1);
}
void init(std::vector<Info> & info) {
int n = info.size();
tr.assign(n << 2, {});
std::function<void(int, int, int)> build = [&](int l, int r, int u) -> void {
tr[u] = {l, r, {},0};
if (l == r) {
tr[u].info = info[l];
return;
}
int mid = l + r >> 1;
build(l, mid, u << 1); build(mid + 1, r, u << 1 | 1);
pushup(u);
};
build(0, n - 1, 1);
}
void pushup(int u) {
tr[u].info = tr[u << 1].info + tr[u << 1 | 1].info;
}
void apply(int idx,int val){
tr[idx].info.sum += 1ll*(tr[idx].r - tr[idx].l + 1) * val;
tr[idx].tag += val, tr[idx].info.mx += val;
}
void pushdown(int idx){
if(tr[idx].tag == 0)return;
apply(idx*2,tr[idx].tag);
apply(idx*2+1,tr[idx].tag);
tr[idx].tag=0;
}
void build(int l, int r, int u = 1) {
tr[u] = {l, r, {}, -1};
if (l == r) {
return;
}
int mid = (l + r) >> 1;
build(l, mid, u << 1); build(mid + 1, r, u << 1 | 1);
pushup(u);
}
void modify(int p, const Info & info, bool set = false) {
int u = 1;
while (tr[u].l != tr[u].r) {
int mid = (tr[u].l + tr[u].r) >> 1;
if (p <= mid) {
u = u << 1;
} else {
u = u << 1 | 1;
}
}
if (set) {
tr[u].info = info;
} else {
tr[u].info = tr[u].info + info;
}
u >>= 1;
while (u) {
pushup(u);
u >>= 1;
}
}
//修改和查询要push_down
void change(int l,int r,int idx, int val){
if(l<=tr[idx].l && tr[idx].r <= r) {apply(idx,val);return;}
pushdown(idx);
int mid = (tr[idx].l + tr[idx].r) >> 1;
if(l<=mid) change(l,r,idx*2,val);
if(mid<r) change(l,r,idx*2+1,val);
pushup(idx);
}
Info query(int l, int r, int u = 1) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].info;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (r <= mid) {
return query(l, r, u << 1);
} else if (l > mid) {
return query(l, r, u << 1 | 1);
}
return query(l, r, u << 1) + query(l, r, u << 1 | 1);
}
};
struct Info{
ll sum,mx;
Info():sum(0), mx(0){}
Info(int x):sum(x), mx(x){}
};
Info operator+(const Info l,const Info r){
Info res;
res.sum=l.sum+r.sum;
res.mx = max(l.mx, r.mx);
return res;
}
// 滑窗找答案 + 区间加,区间max线段树
int work(){
vector<Info> info(n+1);
For(i,1,n) info[i].mx = 0;
SegmentTree<Info> seg(info);
int ans = 0;
for(int r = 0, l = r; r < n && l <= r;){
while(r<n){
auto [L,R] = segs[r];
seg.change(L,R,1,1);
if(seg.query(1,n).mx <= k) ++r;
else {
seg.change(L,R,1,-1);
break;
}
}
ans = max(ans,r-l);
auto [L,R] = segs[l++];
seg.change(L,R,1,-1);
}
return ans;
}
void solve(){
cin >> n >> k, segs.resize(n);
for(auto &[l,r] : segs) cin >> l >> r;
cout << work() << '\n';
}D
排序+抑或哈希
在原来的边上排序后,抑或哈希就可以了
就是一张图上块间求和,块内求抑或哈希,这样就好了,
注意一下每次都将同一个权值的边全部连通再去比较,而不是连一条边就开始比较,因为是无序的,最后只要连通就行了
也不知道最大生成树是个什么东西
ull val[Maxn];
mt19937_64 rng(time(0));
// unordered_map<int,ll> hashing;
// ll get_hash(int x){
// return hashing.count(x) ? hashing[x] : hashing[x] = rng_big();
// }
struct DSU {
vector<int> p, sz;
vector<ull> me;
ull sum;
// 块内抑或,块间加和
DSU(int n = 0){init(n);}
void init(int n){sum = 0;p.resize(n+1); me.resize(n+1); sz.assign(n+1,1); iota(p.begin(),p.end(),0); for(int i=1;i<=n;++i) me[i] = val[i], sum += me[i];}
int find(int x){return p[x]==x?x:p[x]=find(p[x]);}
bool unite(int a,int b){
{
a=find(a); b=find(b); if(a==b) return false; if(sz[a]<sz[b]) swap(a,b);
p[b]=a; sz[a]+=sz[b];
sum -= me[a], sum -= me[b], me[a]^=me[b], sum += me[a];
return true;
}
}
int getsz(int x){return sz[p[x]];}
bool isSame(int u,int v){return find(u) == find(v);}
}dsu, dsu2;
bool work(){
vector<int> todo;
for(auto [w,u,v] : edges1) todo.pb(w);
for(auto [w,u,v] : edges2) todo.pb(w);
sort(all(todo)); todo.erase(unique(all(todo)), todo.end());
int p1 = 0, p2 = 0;
for(auto tow : todo){
while(p1 < edges1.size() && get<0>(edges1[p1]) == tow){
auto [w,u,v] = edges1[p1++];
dsu.unite(u,v);
}
while(p2 < edges2.size() && get<0>(edges2[p2]) == tow){
auto [w,u,v] = edges2[p2++];
dsu2.unite(u,v);
}
if(dsu.sum != dsu2.sum) return 0;
}
return 1;
}
void init(){
For(i,1,n) G1[i].clear(), G2[i].clear(), val[i] = rng();
edges1.clear(), edges2.clear();
dsu.init(n), dsu2.init(n);
}
void solve(){
cin >> n >> m1 >> m2; init();
// cerr << edges1.size() << '\n';
For(i,0,m1-1)cin >> x >> y >> z, edges1.emplace_back(z,x,y);
For(i,0,m2-1)cin >> x >> y >> z, edges2.emplace_back(z,x,y);
sort(all(edges1)); sort(all(edges2));
puts(work() ? "YES" : "NO");
}E
DP 数四元组子序列贡献
思路:
将两色相交弦转为编号递增的四元组 (p1,p2,p3,p4),其中 p1 < p2 < p3 < p4,且 c[p1] = c[p3], c[p2] = c[p4]
贡献就是 a[p1] * a[p3] * a[p2] * a[p4], 这就是一个四元组的贡献,遍历一对颜色 (i,j) 产生的贡献是所有四元组的贡献
这个可以理解成所有单调增四元组子序列的乘积之和, 可以 dp 计算所有子序列的贡献
dp[i]:表示状态为 i 的子序列的所有贡献和,且选择到的子序列状态为:
dp[3]:i,j,i,j,dp[2]:i,j,i,dp[1]:i,j,dp[0]:i
转移:i -> i,j,i,j -> i,j,i,i,j,i -> i,j,i,j,就是每增加一个数就乘到已有的所有序列上去,最后求和就是答案了
具体的,我们可以先生成两种颜色按照升序排序的下标序列,然后再在这个序列上做所有四元组乘积之和的 dp
序列的生成逻辑其实是归并排序,A 序列中的还有数字且 B 用完了或者 A[i] < B[j] 时,才会在结果序列增加 A 的元素,否则就增加 B 中的元素
然后 dp 就好了
ll M(ll x){
if(x >= 0 && x < Mod)return x;
if(x >= Mod && x < 2 * Mod)return x - Mod;
if(x >= -Mod && x < 0)return x + Mod;
if(x > 0)return x % Mod;
return (x % Mod + Mod) % Mod;
}
void solve(){
cin >> n >> k;
vector<vector<int>> ids(k+1);
vector<int> C(n+1), A(n+1);
For(i,1,n) cin >> C[i], ids[C[i]].pb(i);
For(i,1,n) cin >> A[i];
ll ans = 0;
For(i,1,k) For(j,1,k) if(i!=j){
vector<tuple<int,int,int>> op;
int p1 = 0, p2 = 0;
while(p1 < ids[i].size() || p2 < ids[j].size()){
if(p1 < ids[i].size() && (p2 == ids[j].size() || ids[i][p1] < ids[j][p2])) op.emplace_back(ids[i][p1], A[ids[i][p1]], 0), ++p1;
else op.emplace_back(ids[j][p2], A[ids[j][p2]], 1), ++p2;
}
vector<ll> dp(4,0);
for(auto [id, val, tag] : op){
if(tag == 0){
dp[0] = M(dp[0] + val);
dp[2] = M(dp[2] + dp[1]*val);
}else{
dp[1] = M(dp[1] + dp[0]*val);
dp[3] = M(dp[3] + dp[2]*val);
}
}
ans = M(ans + dp[3]);
}
cout << ans << '\n';
}B
随机
k 层间接关系,说白了就是 k 条边花费的最短路,
然后随机性质,大数据大概率有解,
小数据跑暴力任意两点间的最短路用 floyd 跑出来就行了,最大上限 n 是 500 的,n^3 可接受
大数据直接猜
void solve(){
cin >> n >> q;
For(i,1,n) For(j,1,n) mp[i][j] = 1e9;
For(i,1,n-1){
cin >> s;
for(int j=0; j < s.size();++j){
int t = i + j + 1;
if(s[j] == '1') mp[i][t] = 1;
else mp[t][i] = 1;
}
}
if(n<=500){
for(int j=1;j<=n;++j){
for(int i=1;i<=n;++i){
for(int k=1;k<=n;++k) mp[i][k] = min(mp[i][k], mp[i][j] + mp[j][k]);
}
}
while(q--){
cin >> x >> y;
if(mp[x][y] == 1e9) cout << -1 <<'\n';
else cout << mp[x][y] - 1 << '\n';
}
}else{
while(q--){
cin >> x >> y;
if(mp[x][y] == 1) cout << 0 << '\n';
else cout << 1 << '\n';
}
}
}写在最后
放弃是不可能放弃的,我还没到时候呢
有一说一,但凡有点实力也不至于禁赛,真的
但是第一次省赛打成这个鸟样,真的是一辈子的耻辱
















