2026牛客寒假算法基础集训营1部分题解

A. A+B Problem

题意:

给定八个七段数码管显示器,每个显示器有 7 个灯管(编号 1~7)。每个灯管 \(i\) 被点亮的概率为 \(p_i\%\)(独立)。现在需要将这八个显示器分成两排,每排四个,要求:

1. 每个显示器至少有一个灯管被点亮;

2. 每个显示器显示的数字必须是合法的(0~9);

3. 第一排四个显示器组成的四位数 \(A\)(允许前导零)与第二排四个显示器组成的四位数 \(B\) 满足 \(A + B = C\)\(C\) 给定)。

求满足所有条件的概率,结果对 \(998244353\) 取模。

思路:

首先计算单个显示器显示数字 \(d\)(0~9)的概率。每个数字对应一个固定的灯管亮灭模式,对于灯管 \(i\)

  • 若模式中要求点亮,则贡献概率 \(p_i\%\)
  • 若要求熄灭,则贡献概率 \((100-p_i)\%\)

将所有灯管的概率相乘即可。

由于两排显示器相互独立且概率分布相同,枚举数字 \(x\)\(0 \le x \le C\)),计算概率,将每个数字概率相乘即可。
最后答案为 \(\sum_{a=0}^{C} dp[a] \times dp[C-a]\)

代码

点击查看代码
vector<int>g[10]={
	{1,1,1,0,1,1,1},
	{0,0,1,0,0,1,0},
	{1,0,1,1,1,0,1},
	{1,0,1,1,0,1,1},
	{0,1,1,1,0,1,0},
	{1,1,0,1,0,1,1},
	{1,1,0,1,1,1,1},
	{1,0,1,0,0,1,0},
	{1,1,1,1,1,1,1},
	{1,1,1,1,0,1,1}
};

int a[N];
int b[N];

void solve() {
	int n,m;
	cin>>n;
	int inv=qpow(100,mod-2);
	for(int i=1;i<=7;i++){
		cin>>a[i];
		a[i]*=inv;
		a[i]%=mod;
	}
	for(int i=0;i<=n;i++){
		int x=i;
		int p=1;
		for(int j=0;j<4;j++){
			for(int k=0;k<7;k++){
				if(g[x%10][k])p*=a[k+1];
				else p*=mod+1-a[k+1];
				p%=mod;
			}
			x/=10;
		}
		b[i]=p;
	}
	int ans=0;
	for(int i=0;i<=n;i++){
		ans+=b[i]*b[n-i]%mod;
		ans%=mod;
	}
	cout<<ans<<endl;
}

B. Card Game

题意

给定长度为 \(2n\) 的排列,分成两个数组 \(a\)(小苯的牌)和 \(b\)(小红的牌),长度均为 \(n\)。游戏规则如下:

- 每轮比较两人当前手牌的第一张,数字大的一方得一分并移除自己的牌,另一方不得分且牌保留。

- 游戏进行直到一方无牌。

- 小苯可以在游戏开始前任意重排自己的牌,目标是最大化自己的得分。

要求计算能使小苯获得最高得分的不同排列方案数,对 \(998244353\) 取模。

思路

模拟游戏过程不难发现,令 \(mn\) 为小红的最小值,\(m\) 为小苯大于 \(mn\) 的个数,得到的得分为最大可能得分 \(m\)
所有达到最高得分 \(m\) 的排列都满足:所有大于 \(mn\) 的牌(共 \(m\) 张)必须出现在所有小于等于 \(mn\) 的牌(共 \(n-m\) 张)之前。赢的牌内部和输的牌内部可以任意排列。
因此方案数为 \(m! \times (n-m)!\)

代码

点击查看代码
int a[N];
int b[N];

void solve() {
	int n,m;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	int mn=1e9;
	for(int i=1;i<=n;i++){
		cin>>b[i];
		mn=min(mn,b[i]);
	}
	int s=0;
	for(int i=1;i<=n;i++){
		s+=a[i]>mn;
	}
	int ans=1;
	for(int i=1;i<=s;i++){
		ans*=i;
		ans%=mod;
	}
	for(int i=1;i<=n-s;i++){
		ans*=i;
		ans%=mod;
	}
	cout<<ans<<endl;
}

C. Array Covering

题意:

给定一个长度为 \(n\) 的数组 \(a\),可以执行任意次操作:每次选择两个下标 \(l\)\(r\)\(1 \le l < r \le n\)),将开区间 \((l, r)\) 内的所有数替换为 \(\max(a_l, a_r)\)。求操作后数组所有元素之和的最大值。

思路:

由于操作只改变开区间内的数,数组的首位和末位永远无法被改变。其余位置可以通过一系列操作变为全局最大值 \(M\)。答案为 \(M\cdot (n-2)+a_1+a_n\)

时间复杂度 \(O(n)\)

代码

点击查看代码
void solve() {
    int n;
    cin>>n;
    int mx=0;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        mx=max(mx,a[i]);
    }
 
    cout<<mx*(n-2)+a[1]+a[n]<<endl;
}

D. Sequence Coloring

题意:

给定一个长度为 \(n\) 的序列 \(a\)\(a_i > 0\) 表示白色,\(a_i = 0\) 表示黑色。初始可以选择最多 \(k\) 个白色元素染红。之后每一秒,每个红色元素会将其右侧 \(a_i\) 个位置内的所有白色元素染红。求所有白色元素都被染红的最短时间,若无法做到输出 \(-1\)

思路:

首先提取所有白色元素的位置和对应的 \(a\) 值。根据传播规则,将白色元素划分为若干连通块:每个连通块内的白色元素可以通过传播相互到达,不同连通块之间无法传播。若连通块数量 \(blocks > k\),则无法全部染红,输出 \(-1\)

否则,二分答案时间 \(T\)。对于每个连通块,预处理 \(next\) 数组:\(next[i]\) 表示从第 \(i\) 个白色元素出发,一秒内能到达的最右白色元素下标。再用倍增预处理 \(go[i][p]\),表示从 \(i\) 出发 \(2^p\) 秒能到达的最右下标。

对于给定的 \(T\),计算每个连通块内最少需要的起点数:从左到右贪心,每次选择当前起点 \(i\),用倍增快速求出 \(T\) 秒内能覆盖的最右下标 \(R\),然后以 \(R+1\) 作为下一段的起点,计数加一。累加所有连通块的起点数,若总数 \(\le k\)\(T\) 可行。

二分找到最小的可行 \(T\) 即为答案。

代码

点击查看代码

int n, k;
int a[N];
vector<int>b;
vector<PII> q;
vector<vector<int>> nxt;
vector<vector<vector<int>>> go;

bool ok(int T) {
	int sum = 0;
	for (int j = 0; j < q.size(); j++) {
		int l = q[j].first, r = q[j].second;
		int i = l, cnt = 0;
		while (i <= r) {
			++cnt;
			int x = i;
			for (int p = 0; p <= 20; ++p) {
				if ((T >> p) & 1) {
					x = go[j][x - l][p];
				}
			}
			i = x + 1;
		}
		sum += cnt;
		if (sum > k) return false;
	}
	return sum <= k;
}

void solve() {
	cin >> n >> k;
	b.clear();
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		if (a[i] > 0) b.push_back(i);
	}
	int m = b.size();
	if (m == 0) {
		cout << 0 << endl;
		return;
	}
	q.clear();
	int l = 0;
	int x = b[0] + a[b[0]];
	for (int i = 1; i < m; ++i) {
		if (b[i] <= x) {
			x = max(x, b[i] + a[b[i]]);
		} else {
			q.push_back({l, i - 1});
			l = i;
			x = b[i] + a[b[i]];
		}
	}
	q.push_back({l, m - 1});
	if (q.size() > k) {
		cout << -1 << endl;
		return;
	}
	int blocks = q.size();
	nxt.assign(blocks, vector<int>());
	go.assign(blocks, vector<vector<int>>());
	for (int u = 0; u < blocks; u++) {
		int L = q[u].first, R = q[u].second;
		int len = R - L + 1;
		vector<int> &nx = nxt[u];
		nx.resize(len);
		int j = L;
		for (int i = L; i <= R; i++) {
			int lim = b[i] + a[b[i]];
			while (j <= R && b[j] <= lim) j++;
			nx[i - L] = j - 1;
		}
		vector<vector<int>> &g = go[u];
		g.resize(len, vector<int>(20 + 1));
		for (int i = 0; i < len; i++) g[i][0] = nx[i];
		for (int p = 1; p <= 20; p++) {
			for (int i = 0; i < len; ++i) {
				int t = g[i][p - 1];
				g[i][p] = g[t - L][p - 1];
			}
		}
	}

	int L = 0, R = m, ans = -1;
	while (L <= R) {
		int mid = (L + R) >> 1;
		if (ok(mid)) {
			ans = mid;
			R = mid - 1;
		} else {
			L = mid + 1;
		}
	}
	cout << ans << endl;
}


E. Block Game

题意:

给定一个长度为 \(n\) 的数组 \(a\) 和一个整数 \(k\)。每次操作可以将当前的万能方块 \(k'\) 插入数组左侧,数组整体右移,最右侧的数字成为新的万能方块。可以进行任意次操作(包括 \(0\) 次)。求最终数组的第一个元素与万能方块的和的最大值。

思路:

操作相当于将整个数组 \(C = [k, a_1, a_2, \dots, a_n]\) 循环左移。因此问题转化为在循环数组 \(C\) 中找相邻两个元素和的最大值,即 \(\max\limits_{i=0}^{n} (C[i] + C[(i+1) \bmod (n+1)])\)

代码

点击查看代码
void solve() {
    int n,m;
    cin>>n>>m;
    int mx=-1e18;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    a[0]=m;
    for(int i=1;i<=n;i++){
        mx=max(mx,a[i]+a[i-1]);
    }
    mx=max(mx,a[0]+a[n]);
    cout<<mx<<endl;
}

G. Digital Folding

题意:

给定一个区间 \([l, r]\),对于区间内的每个整数 \(i\),定义其折叠数为:将 \(i\) 的十进制表示反转并去掉前导零后得到的整数。求所有 \(i\) 的折叠数的最大值。

思路:

观察发现,要使折叠数尽可能大,需要让反转后的最高位尽可能大。对于任意一个不超过 \(r\) 的数,我们可以将其调整为以若干个9结尾的形式,这样反转后最高位就是9。

具体做法是:枚举以 \(i\) 个9结尾的数,通过 \(x = \lfloor \frac{r}{p} \rfloor \times p - 1\) 构造出不大于 \(r\) 且以 \(i\) 个9结尾的最大数(其中 \(p = 10^i\))。如果这个数在 \([l, r]\) 区间内,就用它的折叠数更新答案。同时,\(r\) 本身的折叠数也需要考虑。

代码

点击查看代码
int get(int x){
    string s=to_string(x);
    reverse(s.begin(),s.end());
    return stoll(s);
}
 
void solve() {
    int l,r;
    cin>>l>>r;
    int mx=get(r);
    int p=1;
    for(int i=0;i<18;i++){
        int x=r/p*p-1;
        if(x<l)break;
        mx=max(mx,get(x));
         
        p*=10;
 
    }
    cout<<mx<<endl;
     
}

H. Blackboard

题意:

给定数组 \(a_1, a_2, \dots, a_n\)。现在要将数组分成若干连续段,每段内计算所有数的按位或,然后将各段的结果相加。要求这个和等于所有 \(a_i\) 的和。求分割方案数。

思路:

设原数组和为 \(S = \sum a_i\)。对于一种分割,每段 \([l, r]\) 的值为 \(o = \text{OR}_{i=l}^r a_i\)。令所有段的 \(o\) 之和为 \(T\)。需要 \(T = S\)

注意到对于任意一段,由于按位或的性质,有 \(o \ge \max_{i \in [l, r]} a_i\),且通常 \(o\) 小于等于段内数字的和。实际上,可以证明 \(T = S\) 当且仅当对于每一段,段内所有数字的二进制表示互不重叠(即任意两个数字的按位与为 \(0\))。这是因为如果段内数字有重叠的二进制位,那么 \(o\) 会严格小于段内和,导致 \(T < S\);如果所有段都满足不重叠,则 \(o\) 等于段内和,从而 \(T = S\)

因此,问题转化为:将序列分成若干连续段,使得每一段内任意两个数字的按位与均为 \(0\)。求分割方案数。

可以用动态规划解决。设 \(dp[i]\) 表示前 \(i\) 个数字的分割方案数。转移时,枚举最后一段的起点 \(j+1\),要求 \([j+1, i]\) 满足条件,则 \(dp[i] = \sum dp[j]\)。直接转移是 \(O(n^2)\) 的。

优化:对于每个右端点 \(i\),可以求出最小的左端点 \(L_i\),使得 \([L_i, i]\) 满足条件,并且对于任意 \(L \ge L_i\)\([L, i]\) 也满足条件(因为子段必然满足)。那么 \(dp[i] = \sum_{j = L_i-1}^{i-1} dp[j]\)。可以用前缀和优化到 \(O(1)\) 转移。

使用双指针维护当前窗口 \([L, i]\) 的按位或值,并维护每个二进制位出现的次数。当加入 \(a_i\) 时,如果当前按位或与 \(a_i\) 有重叠的二进制位(即按位与不为 \(0\)),则不断右移 \(L\),并更新按位或,直到无重叠。此时 \(L_i = L\)。由于 \(L_i\) 单调不减,总复杂度为 \(O(n \cdot B)\),其中 \(B = 31\)

代码

点击查看代码
void solve() {
	cin >> n;
	for (int i = 0; i <= n + 2; i++) {
		b[i] = c[i] = 0;

	}
	for (int j = 0; j <= 31; j++) {
		lst[j] = 0;
	}
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	b[0] = c[0] = 1;
	int l = 1;
	for (int i = 1; i <= n; i++) {
		int nt = 1;
		for (int k = 0; k < 31; k++) {
			if ((a[i] >> k) & 1) {
				nt = max(nt, lst[k] + 1);
			}
		}
		l = max(l, nt);
		b[i] = c[i - 1];
		if (l >= 2) b[i] = (b[i] - c[l - 2] + mod) % mod;
		c[i] = (c[i - 1] + b[i]) % mod;
		for (int k = 0; k < 31; k++) {
			if ((a[i] >> k) & 1) lst[k] = i;
		}
	}
	cout<<b[n]<<endl;


}

I. AND vs MEX

题意:

给定区间 \([l, r]\),每次操作可以从区间中选取任意个数字,计算它们的按位与,并将结果加入集合 \(S\)。可以进行任意次操作,求集合 \(S\) 的 MEX的最大值。

思路:

观察发现,由于 AND 操作的结果不会超过 \(r\),因此答案上界为 \(r+1\)。根据 \(l\)\(r\) 的最高位情况进行分类讨论:

  • \(l = 0\),则区间包含 \(0\),且每个数自身 AND 得到本身,故能构造出 \(0\)\(r\) 的所有数,答案为 \(r+1\)
  • \(l\)\(r\) 的最高位相同,则区间所有数在该位均为 \(1\),任何 AND 结果该位也为 \(1\),无法得到 \(0\),答案为 \(0\)
  • \(r\) 的最高位比 \(l\) 的最高位至少高两位,则可以构造出 \(0\)\(r\) 的所有数,答案为 \(r+1\)
  • \(r\) 的最高位恰好比 \(l\) 的最高位高一位,则记 \(A = r - 2^{\text{highbit}(r)}\),以及 \(low\_l\)\(l\) 去掉从最高位开始连续的 \(1\) 右侧部分后得到的值(即保留最高位及其后的连续 \(1\),后面全置 \(0\))。若 \(low\_l \le A+1\),则可以构造出 \(0\)\(r\) 的所有数,答案为 \(r+1\);否则答案为 \(A+1\)

代码

点击查看代码
void solve() {
	int l, r;
	cin >> l >> r;
	if (l == 0) {
		cout << r + 1 << endl;
		return;
	}
	int b1 = 31 - __builtin_clz(l);
	int b2 = 31 - __builtin_clz(r);
	if (b1 == b2) {
		cout << 0 << endl;
		return;
	}
	if (b2 >= b1 + 2) {
		cout << r + 1 << endl;
		return;
	}
	int ans = r - (1 << b2);
	int p = b1;
	while (p >= 0 && (l >> p) & 1) p--;
	int L;
	if (p < 0) L = l;
	else L = l & (~((1 << (p + 1)) - 1));
	if (L <= ans + 1) cout << r + 1 << endl;
	else cout << ans + 1 << endl;
}

K. Constructive

题意:

给定正整数 \(n\),构造长度为 \(n\) 的正整数数组,满足:元素互不相同,且所有元素的乘积等于所有元素的和。如果存在,输出字典序最小的解;否则报告无解。

思路:

\(n=1\) 时,数组 \([1]\) 满足条件。
\(n=3\) 时,数组 \([1,2,3]\) 满足条件。
对于其他 \(n\),可以证明不存在满足条件的数组(\(n=2\) 时无解;\(n \ge 4\) 时,数组的积大于数组的和)。

代码

点击查看代码
void solve() {
    int n;
    cin>>n;
    if(n==1){
        cout<<"YES\n1\n";
    }else if(n==3){
        cout<<"YES\n1 2 3\n";
    }else{
        cout<<"NO\n";
    }
}

L. Need Zero

题意:

给定一个正整数 n,你需要选择一个正整数 x,使得 n*x 的个位数是 0。操作必须恰好执行一次。求最小的 x。

思路:

要使 n*x 的个位为 0,即 n*x 是 10 的倍数。由于 10=2*5,考虑 n 是否被2,5整除,对应结果输出即可。

代码

点击查看代码
void solve() {
    int n;
    cin>>n;
    if(n%10==0){
        cout<<1<<endl;
    }else if(n%2==0){
        cout<<5<<endl;
    }else if(n%5==0){
        cout<<2<<endl;
    }else{
        cout<<10<<endl;
    }
}
posted @ 2026-02-03 19:03  ptlks  阅读(10)  评论(0)    收藏  举报