写在前面

人生第一次 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';
        }
    }
}

写在最后

放弃是不可能放弃的,我还没到时候呢

有一说一,但凡有点实力也不至于禁赛,真的

但是第一次省赛打成这个鸟样,真的是一辈子的耻辱