长春集训 Day 11 - 模拟赛

长春集训 Day 11 - 模拟赛

85+70+10+30

T1 - 操作序列

Description

给定正整数 \(x\),可以进行如下两种操作:

  • \(x \leftarrow 2x-1\)
  • \(x\) 为奇数,则 \(x \leftarrow \dfrac{x+1}2\),否则无法进行该操作。

求出长度为 \(n\) 的合法操作序列数对 \(998244353\) 取模的值。

\(1 \le n \le 5000\)

Solution

容易注意到,这两种操作具有明显的互逆性。具体地,无论 \(x\) 是奇是偶,它先经操作 1 再经操作 2 一定能够回到原数;当 \(x\) 为奇时,它先经操作 2 再经操作 1 才能回到原数。因此不断的进行操作 2,可以得到最终 \(x\) 能递推到的最小值。

然后注意到我们递推的结构类似于一个树状图,而且能够递推出的数很离散,所以显然可以对数种分类 DP。具体地,设 \(dp_{i,j}\) 表示目前递推到第 \(i\) 层,最后一位为第 \(j\) 种数的方案数。显然它只能来源于第 \(i-1\) 层的 \(j-1\)\(j+1\),直接 DP 即可。

需要特判 \(x=1\) 的情况,因为无论它如何操作,数字都不会变。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 5005, mod = 998244353;
int n, x;
int ans, dp[N][N];
int cnt = 0;
int qpow(int x, int y) {
	if (y == 0) return 1;
	int tmp = qpow(x, y/2);
	if (y&1) return tmp * tmp % mod * x % mod;
	return tmp * tmp % mod;
}
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	// file(operation);
	cin >> n >> x;
	if (x == 1) {
		cout << qpow(2, n);
		return 0;
	}
	while (x&1) {
		x = (x+1) / 2;
		cnt++;
	}
	dp[0][cnt] = 1;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= n+cnt; j++) {
			dp[i][j] = (dp[i-1][j+1] + dp[i-1][j-1]) % mod;
		}
	}
	for (int i = 0; i <= n+cnt; i++) {
		ans = (ans + dp[n][i]) % mod;
	}
	cout << ans;
	return 0;
}

T2 - 考场划分

Description

给定长度为 \(n\) 的数列 \(a\),试将 \(a\) 划分为若干连续非空段,使得段长极差不超过 \(k\),段内元素之和的极差不超过 \(S\),求划分方案数对 \(998244353\) 取模的结果。

\(1 \le n \le 100\)

Solution

70pts 做法:

段长极差和段内元素之和的极差非常不好维护,但注意到合法的段长只有 \(O(n)\) 种,合法的段内元素之和只有 \(O(n^2)\) 种,所以可以直接枚举。

假设目前枚举的段长为 \(P\),段内元素之和为 \(Q\),记 \(dp_{i}\) 表示考虑到第 \(i\) 位的划分方案数,可以枚举前缀,产生新的段长和段内元素之和来转移。不过,我们还需满足极差的两条限制,所以不能这样简单地定义状态。这时可以人为地加上限制,即钦定:对于目前枚举的 \(P,Q\),所有 \(dp\) 转移过程中枚举到的段长必须属于区间 \([P,P+k]\),段内元素之和必须属于区间 \([Q,Q+S]\)。这样可以自动满足极差的限制。但这样不能确定段长和段内元素之和恰好为 \(P\)\(Q\) 的状态值,所以我们加两维,定义出 \(dp_{i,0/1,0/1}\),表示目前状态中是否出现了恰好为 \(P\)\(Q\) 的段长或段内元素之和。最终,对于每对枚举的 \(P,Q\),我们将 \(dp_{n,1,1}\) 纳入对答案的贡献即可。时间复杂度是 \(O(n^5)\) 的。

可以用前缀和优化到 \(O(n^4)\),但我没有搞明白。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 105, mod = 998244353;
int n, k, S;
int a[N], sum[N], ans;
int dp[N][2][2];
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	file(exam);
	cin >> n >> k >> S;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		sum[i] = sum[i-1] + a[i];
	}
	vector<int> sums;
    for (int l = 1; l <= n; l++) {
        for (int r = l; r <= n; r++) {
            sums.push_back(sum[r] - sum[l-1]);
        }
    }
	sort(sums.begin(), sums.end());
	sums.erase(unique(sums.begin(), sums.end()), sums.end());
	for (int P = 1; P <= n; P++) {
		for (int Q : sums) {
			memset(dp, 0, sizeof dp);
			dp[0][0][0] = 1;
			for (int i = 1; i <= n; i++) {
				for (int j = 0; j < i; j++) {
					// for that: len == P, sum == Q
					/*
					1          j      i
					------------
					===================
					cur_P = i-j+1, cur_Q = s_i - s_{j-1}
					*/
					int cur_P = i-j, cur_Q = sum[i] - sum[j];
					if (cur_P < P or cur_P > P + k) continue;
					if (cur_Q < Q or cur_Q > Q + S) continue;
					bool ext_P = (cur_P == P);
					bool ext_Q = (cur_Q == Q);
					for (int p = 0; p <= 1; p++) {
						for (int q = 0; q <= 1; q++) {
							dp[i][p|ext_P][q|ext_Q] = (dp[i][p|ext_P][q|ext_Q] + dp[j][p][q]) % mod;
						}
					}
				}
			}
			ans = (ans + dp[n][1][1]) % mod;
		}
	}
	cout << ans;
	return 0;
}

T3 - 区间操作

Description

给定长度为 \(n\) 的序列 \(a\)。对于区间 \([l,r]\),初始持有数字 \(s=0\),接下来可以进行无限次操作,每次操作选中一个 \(a_x(x \in [l,r])\),令 \(s,a_x \leftarrow \dfrac{s+a_x}2\)。可以证明,能通过适当的策略获得最大的 \(s\)

已知在这 \(\dfrac{n(n+1)}{2}\) 种选择方案中,计算所有方案下能够获得的最大的 \(s\) 的加和对 \(998244353\) 取模的结果。

\(1 \le n \le 5 \times 10^5\)

Solution

容易发现,对于任意区间,最大值总和等于所有元素在该区间内按降序排列后贡献的权重和。形式化地,设区间 \([l,r]\) 内的元素从大到小排序得到 \(b_1 \ge b_2 \ge \cdots \ge b_m\),则

\[s_{\max} = \sum _{i=1}^n \frac {1}{2^i} \cdot b_i \]

据此,第 \(k\) 大的数贡献权重为 \(\dfrac 1 {2^k}\)

固定元素 \(a_i\),考虑所有包含它的区间 \([l,r]\),记 \([l,i-1]\) 中大于 \(x\) 的数的个数为 \(L(l)\);右侧 \([i+1,r]\) 中大于 \(x\) 的数的个数为 \(R(r)\),则该元素在所有包含它的区间上产生的贡献为

\[\begin{aligned} & a_i \cdot \sum_{l=1}^i \sum_{r=i}^n \frac 1{2^{L(l)+R(r)+1}} \\ =& a_i \cdot \sum_{l=1}^i \sum_{r=i}^n \Big(\frac 1{2^{L(l)}} \cdot \frac 1{2^{R(r)}} \cdot \frac 12\Big) \\ =& \frac {a_i}2 \Big(\sum_{l=1}^i \frac 1{2^{L(l)}}\Big) \Big(\sum_{r=i}^n \frac 1{2^{R(r)}}\Big) \end{aligned} \]

需要快速计算括号里的两项。

考虑一个动态的思想:一边往数组中加元素,一边计算答案。可以将数组排序,然后从大到小依次处理每个元素。在原数组上种植一棵线段树,维护:

  • 节点区间的带权和,即 \(\sum \dfrac 1{2^k}\),其中 \(k\) 为排名。
  • 节点区间内已插入的元素个数,即大于当前值的元素数量。

然后简单搞一搞就能实现查询了。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 5e5+5, mod = 998244353;
const int inv2 = (mod + 1) / 2;
int n, p2[N], ip2[N];
struct Element {
	int val, id;
	friend bool operator < (Element x, Element y) {
		return x.val < y.val;
	}
} a[N];
struct SegmentTree {
	#define mid ((l + r) / 2)
	#define lson (id << 1)
	#define rson (id << 1 | 1)
	int tcnt = 0;
	struct Node {
		int sum, cnt; // 加权和;区间内插入的元素数
	} t[N << 2];
	void pushup(int id) {
		t[id].cnt = t[lson].cnt + t[rson].cnt;
		t[id].sum = (t[lson].sum * ip2[t[rson].cnt] + t[rson].sum) % mod;
	}
	void build(int id, int l, int r) {
		t[id].sum = 1;
		t[id].cnt = 0;
		if (l == r) {
			return;
		} else {
			build(lson, l, mid);
			build(rson, mid + 1, r);
			pushup(id);
		}
	}
	void modify(int id, int l, int r, int pos) {
		if (l == r) {
			t[id].sum = t[id].sum * inv2 % mod;
			t[id].cnt++;
			return;
		} else {
			if (pos <= mid) modify(lson, l, mid, pos);
			else modify(rson, mid + 1, r, pos);
			pushup(id);
		}
	}
	int query(int id, int l, int r, int qr) {
		if (l > qr) return 0;
		if (r <= qr) {
			tcnt += t[id].cnt;
			return t[id].sum * ip2[t[1].cnt - tcnt] % mod;
		}
		return (query(lson, l, mid, qr) + query(rson, mid + 1, r, qr)) % mod;
	}
	int query(int qr) {
		tcnt = 0;
		return query(1, 1, n, qr) * p2[t[1].cnt - tcnt] % mod;
	}
	#undef mid
	#undef lson
	#undef rson
} segl, segr;
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	// file(sequence);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i].val;
		a[i].id = i;
	}
	p2[0] = ip2[0] = 1;
	for (int i = 1; i <= n; i++) {
		p2[i] = p2[i-1] * 2 % mod;
		ip2[i] = ip2[i-1] * inv2 % mod;
	}
	sort(a+1, a+1+n);
	segl.build(1, 1, n);
	segr.build(1, 1, n);
	int ans = 0;
	for (int i = n; i >= 1; i--) {
		ans = (ans + segl.query(a[i].id) * segr.query(n-a[i].id+1) % mod * a[i].val) % mod;
		segl.modify(1, 1, n, a[i].id);
		segr.modify(1, 1, n, n-a[i].id+1);
	}
	cout << ans * inv2 % mod;
	return 0;
}
posted @ 2026-08-09 22:20  L-Coding  阅读(7)  评论(0)    收藏  举报