第十六届蓝桥杯大赛软件赛C/C++大学B组题解
目录
- 试题A: 移动距离
- 试题B: 客流量上限
- [试题C: 可分解的正整数](https://www.dotcpp.com/oj/problem3319.html)
- [试题D: 产值调整](https://www.dotcpp.com/oj/problem3320.html)
- [试题E: 画展布置](https://www.dotcpp.com/oj/problem3321.html)
- [试题F: 水质检测](https://www.dotcpp.com/oj/problem3322.html)
- [试题G: 生产车间](https://www.dotcpp.com/oj/problem3323.html)
- [试题H: 装修报价](https://www.dotcpp.com/oj/problem3324.html)
试题A: 移动距离
问题描述
小明初始在二维平面的原点,他想前往坐标(233,666)。在移动过程中,他只能采用以下两种移动方式,并且这两种移动方式可以交替、不限次数地使用:
- 水平向右移动,即沿着x轴正方向移动一定的距离。
- 沿着一个圆心在原点(0,0)、以他当前位置到原点的距离为半径的圆的圆周移动,移动方向不限(即顺时针或逆时针移动不限)。
在这种条件下,他到达目的地最少移动多少单位距离?你只需要输出答案四舍五入到整数的结果。
解题思路
这个问题可以转化为:如何从原点到达目标点,使得路径长度最小。
首先,我们可以观察到,如果目标点在x轴上,那么直接水平向右移动是最优的。
对于一般情况,我们可以分两步走:
- 先沿着x轴正方向移动到点(r,0),其中r是目标点到原点的距离
- 然后沿着半径为r的圆弧移动到目标点
目标点(233,666)到原点的距离为:
r = √(233² + 666²) = √(54289 + 443556) = √497845 ≈ 705.58
第一步移动距离为r = 705.58
第二步需要计算圆弧长度。目标点与x轴正方向的夹角为θ:
有sinθ = 666 / r 故θ = arcsin(666 / r) ≈ 1.23426;
圆弧长度 = r·θ = 705.58 × 1.23426 ≈ 870.87
总移动距离 = 705.58 + 870.87 = 1576.45
四舍五入到整数为1576。
asin(666 / 705.58) * 705.58 + 705.58
答案
1576
试题B: 客流量上限
问题描述
一家连锁旅馆在全国拥有2025个分店,分别编号为1至2025。随着节日临近,总部决定为每家分店设定每日客流量的上限,分别记作A₁,A₂,…,A₂₀₂₅。这些上限并非随意分配,而是需要满足以下约束条件:
- A₁,A₂,…,A₂₀₂₅必须是1至2025的一个排列,即每个A_i均是1至2025之间的整数,且所有A_i互不相同。
- 对于任意分店i和j(1≤i,j≤2025,i可等于j),它们的客流量上限A_i和A_j的乘积不得超过i×j+2025。
现在,请你计算这样的分配方案究竟有多少种。由于答案可能很大,你只需输出其对10^9+7取余后的结果即可。
解题思路
这个问题要求我们计算满足特定约束条件的排列数量。让我们逐步分析这个问题:
约束条件分析
- A₁, A₂, …, A₂₀₂₅ 必须是 1 到 2025 的一个排列
- 对于任意 i 和 j (1 ≤ i,j ≤ 2025),必须满足 Aᵢ × Aⱼ ≤ i × j + 2025
关键观察
首先,我们考虑当 i = j 时的特殊情况:
- 此时约束条件变为:Aᵢ × Aᵢ ≤ i × i + 2025
- 即:Aᵢ² ≤ i² + 2025
- 因此:Aᵢ ≤ √(i² + 2025)
通过计算和分析,我们可以得出以下结论:
-
对于 1014 ≤ i ≤ 2025 的情况:
- 约束条件要求 Aᵢ ≤ i
- 由于 Aᵢ 必须是 1 到 2025 之间的整数,且所有 Aᵢ 互不相同
- 最优解是 Aᵢ = i(这样可以最大化其他位置的选择空间)
-
对于 1 ≤ i ≤ 1012 的情况:
- 约束条件允许 Aᵢ ≤ i + 1
- 对于这些位置,我们有两种选择:Aᵢ = i 或 Aᵢ = i + 1
-
对于 1013 ≤ i ≤ 1013 的情况:
- 这个位置的值已经被前面的分析确定
组合计算
根据上述分析:
- 对于位置 1014 到 2025(共 1012 个位置),每个位置的值唯一确定为 Aᵢ = i
- 对于位置 1 到 1012(共 1012 个位置),每个位置有两种可能的选择
- 因此,总的方案数为 2¹⁰¹²
由于答案可能很大,我们需要对 10⁹ + 7 取模。最终答案为:
2¹⁰¹² mod (10⁹ + 7) = 781448427
代码实现
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7;
const int N = 2025;
// 计算2的幂次取模
long long pow_mod(long long base, long long exp, long long mod) {
long long result = 1;
base %= mod;
while (exp > 0) {
if (exp & 1) {
result = (result * base) % mod;
}
base = (base * base) % mod;
exp >>= 1;
}
return result;
}
int main() {
// 根据分析,答案为2^1012 mod (10^9 + 7)
long long ans = pow_mod(2, 1012, MOD);
cout << ans << endl;
return 0;
}
答案:
781448427
试题C: 可分解的正整数
问题描述
定义一种特殊的整数序列,这种序列由连续递增的整数组成,并满足以下条件:
- 序列长度至少为3。
- 序列中的数字是连续递增的整数(即相邻元素之差为1),可以包括正整数、负整数或0。
例如,[1,2,3]、[4,5,6,7]和[-1,0,1]是符合条件的序列,而[1,2](长度不足)和[1,2,4](不连续)不符合要求。
现给定一组包含N个正整数的数据A₁,A₂,…,A_N。如果某个A_i能够表示为符合上述条件的连续整数序列中所有元素的和,则称A_i是可分解的。请你统计这组数据中可分解的正整数的数量。
解题思路
代码实现
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
int t;
int ans = 0;
cin>>n;
for(int i = 0;i < n;i++){
cin>>t;
if(t != 1){
ans++;
}
}
cout<<ans<<endl;
return 0;
}
试题D: 产值调整
问题描述
偏远的小镇上,三兄弟共同经营着一家小型矿业公司"兄弟矿业"。公司旗下有三座矿山:金矿、银矿和铜矿,它们的初始产值分别用非负整数A、B和C表示。
为了稳定经营,三兄弟设计了一个产值调整策略,每年执行一次,每次调整时,将根据当前的产值A、B、C,计算新产值:
- 金矿新产值A′=⌊(B+C)/2⌋;
- 银矿新产值B′=⌊(A+C)/2⌋;
- 铜矿新产值C′=⌊(A+B)/2⌋。
其中,⌊⌋表示向下取整。计算出A′、B′、C′后,同时更新:A变为A′,B变为B′,C变为C′,作为下一年调整的基础。
三兄弟计划连续执行K次调整。现在,请你帮他们计算,经过K次调整后,金矿、银矿和铜矿的产值分别是多少。
解题思路
我们可以直接模拟这个过程,但由于K可能很大(最大10^9),直接模拟会超时。
观察调整规则,我们可以发现:
- 如果A=B=C,那么调整后仍然是A=B=C
- 如果不相等,每次调整后的值会趋向于平均值
实际上,经过足够多次调整后,三个值会变得相等或者在一个很小的范围内循环。
通过数学分析和实验,我们可以发现:
- 如果初始值全部相等,那么调整后仍然相等
- 如果不全相等,经过最多6次调整,三个值要么全部相等,要么会进入一个长度不超过3的循环
因此,我们可以先模拟前几次调整,然后根据情况决定最终结果。
代码实现
#include <bits/stdc++.h>
using namespace std;
void adjust(long long& a, long long& b, long long& c) {
long long new_a = (b + c) / 2;
long long new_b = (a + c) / 2;
long long new_c = (a + b) / 2;
a = new_a;
b = new_b;
c = new_c;
}
int main() {
int T;
cin >> T;
while (T--) {
long long A, B, C, K;
cin >> A >> B >> C >> K;
// 如果已经相等,不需要调整
if (A == B && B == C) {
cout << A << " " << B << " " << C << endl;
continue;
}
while(K--){
adjust(A,B,C);
// 如果已经相等,不需要调整
if (A == B && B == C) {
break;
}
}
cout << A << " " << B << " " << C << endl;
}
return 0;
}
答案
对于样例输入:
-
A=10, B=20, C=30, K=1
-
一次调整后:A′=25, B′=20, C′=15
-
A=5, B=5, C=5, K=3
-
初始值已经相等,调整后仍然是A=5, B=5, C=5
试题E: 画展布置
问题描述
画展策展人小蓝和助理小桥为即将举办的画展准备了N幅画作,其艺术价值分别为A₁,A₂,…,A_N。他们需要从这N幅画中挑选M幅,并按照一定顺序布置在展厅的M个位置上。
为了优化布置,他们希望使艺术价值的变化程度通过一个数值L来衡量,且该值越小越好。数值L的定义为:
L = Σ(i=1 to M-1) |B²ᵢ₊₁ - B²ᵢ|
其中B_i表示展厅第i个位置上画作的艺术价值。
现在,他们希望通过精心挑选和排列这M幅画作,使L达到最小值。请你帮他们计算出这个最小值是多少。
解题思路
这个问题要求我们从N幅画中选择M幅,并排列它们,使得相邻画作艺术价值平方的差的绝对值之和最小。
首先,我们可以观察到,对于任意两幅画的艺术价值a和b,|a² - b²| = |a-b|·|a+b|。这意味着,如果我们想要最小化|a² - b²|,我们应该选择艺术价值接近的画作放在相邻位置。
一个直观的策略是:
- 对所有画作的艺术价值进行排序
- 选择M幅连续的画作(因为连续的画作艺术价值差异最小)
- 按照特定顺序排列这M幅画作,使得L最小
对于排列顺序,我们可以证明,最优的排列方式是按照艺术价值从小到大或从大到小排列。
代码实现
#include <bits/stdc++.h>
using namespace std;
int main() {
int N, M;
cin >> N >> M;
vector<int> values(N);
for (int i = 0; i < N; i++) {
cin >> values[i];
}
// 排序
sort(values.begin(), values.end());
// 计算所有可能的连续M幅画作的L值
long long min_L = LLONG_MAX;
for (int i = 0; i <= N - M; i++) {
vector<int> selected(values.begin() + i, values.begin() + i + M);
// 计算按照从小到大排列的L值
long long L1 = 0;
for (int j = 0; j < M - 1; j++) {
long long diff = (long long)selected[j+1] * selected[j+1] - (long long)selected[j] * selected[j];
L1 += abs(diff);
}
// 计算按照从大到小排列的L值
long long L2 = 0;
for (int j = 0; j < M - 1; j++) {
long long diff = (long long)selected[M-j-2] * selected[M-j-2] - (long long)selected[M-j-1] * selected[M-j-1];
L2 += abs(diff);
}
min_L = min(min_L, min(L1, L2));
}
cout << min_L << endl;
return 0;
}
答案
对于样例输入:
N=4, M=2, 艺术价值为[1, 5, 2, 4]
排序后为[1, 2, 4, 5]
可能的连续2幅画作为:[1,2], [2,4], [4,5]
计算L值:
- [1,2]: |2² - 1²| = |4 - 1| = 3
- [2,4]: |4² - 2²| = |16 - 4| = 12
- [4,5]: |5² - 4²| = |25 - 16| = 9
最小的L值为3。
试题F: 水质检测
解题思路
题目要求在一个2×n的河床上增加最少的检测器,使得所有检测器互相连通。河床用一个2×n的字符矩阵表示,其中’#‘表示已有检测器,’.'表示空白位置。如果两个检测器上下或左右相邻,则它们互相连通,且连通具有传递性。
这道题目可以使用动态规划来解决。我们需要考虑如何让所有检测器连通,并且使新增的检测器数量最少。
状态定义
我们定义状态dp[i][j]
表示:当处理到第i列,且第i列的第j行为’#'(无论是原有的还是新放置的),并且前i列的所有检测器都连通时,需要新增的最少检测器数量。
其中:
- i表示列号,范围是[st, en],st是最左边有检测器的列,en是最右边有检测器的列
- j表示行号,取值为0或1,分别表示第一行和第二行
状态转移
对于位置(i,j),我们有两种可能的转移来源:
- 从(i-1,j)转移:即上一列同一行的位置
- 从(i-1,1-j)转移:即上一列不同行的位置,但这需要通过当前列的另一行(i,1-j)连接
下面用图示来说明状态转移:
列i-1 列i
□ ---- □ (行0)
| |
□ ---- □ (行1)
假设我们当前要计算dp[i][0]
(即列i 行0的状态):
- 从
dp[i-1][0]
转移:
列i-1 列i
# ---- # (行0)
| |
□ □ (行1)
- 如果(i,0)是’#‘,且(i-1,0)也是’#',它们自然连通。
- 如果(i,0)不是’#',需要放置一个检测器,花费+1。
- 从
dp[i-1][1]
转移:
列i-1 列i
□ # (行0)
| |
# ---- □ (行1)
- 如果(i,0)是’#‘,且(i-1,1)也是’#‘,要与(i,0)连通,需要通过(i,1),当(i,1)不是’#'时,就需要放置一个检测器,故花费+(
s[i][1]
== ‘#’ ? 0 : 1)。 - 如果(i,0)不是’#',那么(i,0)需要放置一个检测器,花费+(
s[i][0]
== ‘#’ ? 0 : 1)。
状态转移方程如下:
- 如果
s[i][j]
已经是’#'(已有检测器):
dp[i][j] = min(dp[i-1][j], dp[i-1][1-j] + (s[i][1-j]=='#'?0:1))
- 如果
s[i][j]
是’.'(需要放置检测器):
dp[i][j] = min(dp[i-1][j], dp[i-1][1-j] + (s[i][1-j]=='#'?0:1)) + 1
边界条件
对于起始位置st(最左边有检测器的位置):
- 如果
s[st][0]
是’#',则dp[st][0]
=0 - 如果
s[st][0]
是’.',则dp[st][0]=1
(需要放置一个检测器) - 同理处理
dp[st][1]
如果起始位置两行都有检测器,需要确保它们连通,此时dp[st][0]
=dp[st][1]
=0。
最终结果
最终答案是min(dp[en][0]
, dp[en][1]
),表示在最右边有检测器的位置en,使得所有检测器连通的最小花费。
代码实现
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6+30;
char s[N][2];
int dp[N][2]; // dp[i][j]表示当前第i列第j行为'#'且与前面的检测器连通时的最小花费
void way(){
string t;
int n;
// 数据输入
for(int j=0;j<2;j++){
cin>>t;// 读入一个字符串
n = t.size();
for(int i=1;i<=n;i++)s[i][j] = t[i-1];
}
int st = n+1,en = 0; // 找到左右两边的起始位置(最左边有检测器的位置)和(最右边有检测器的位置)。
for(int i=1;i<=n;i++){
if(s[i][0]=='#'||s[i][1]=='#'){
st = min(i,st);
en = max(en,i);
}
}
//如果没有检测器直接输出0
if(st == n + 1){
cout<<0<<endl;
return;
}
// 初始化dp数组
memset(dp, 0x3f, sizeof(dp)); // 初始化为一个很大的值
// 处理起始位置
if(s[st][0]=='#') dp[st][0]=0; // 如果已经有检测器,花费为0
else dp[st][0]=1; // 否则需要放置一个检测器,花费为1
if(s[st][1]=='#') dp[st][1]=0; // 如果已经有检测器,花费为0
else dp[st][1]=1; // 否则需要放置一个检测器,花费为1
// 如果起始位置两行都有检测器,需要确保它们连通
if(s[st][0]=='#' && s[st][1]=='#') {
dp[st][0] = dp[st][1] = 0; // 两个位置都已有检测器,花费为0
}
for(int i=st+1;i<=en;i++){
// 计算dp[i][0]
if(s[i][0]=='#') { // 如果当前位置已有检测器
// 从上一列转移
dp[i][0] = min(dp[i-1][0], dp[i-1][1] + (s[i][1]=='#'?0:1));
} else { // 如果当前位置需要放置检测器
dp[i][0] = min(dp[i-1][0], dp[i-1][1] + (s[i][1]=='#'?0:1)) + 1;
}
// 计算dp[i][1]
if(s[i][1]=='#') { // 如果当前位置已有检测器
// 从上一列转移
dp[i][1] = min(dp[i-1][1], dp[i-1][0] + (s[i][0]=='#'?0:1));
} else { // 如果当前位置需要放置检测器
dp[i][1] = min(dp[i-1][1], dp[i-1][0] + (s[i][0]=='#'?0:1)) + 1;
}
}
cout<<min(dp[en][0],dp[en][1])<<endl;
}
int main(){
way();
return 0;
}
示例分析
让我们以样例为例,详细分析算法的执行过程:
输入:
.##.....#
.#.#.#...
我们可以将其表示为一个2×9的矩阵(为了方便,列号从1开始):
列号: 1 2 3 4 5 6 7 8 9
行0: . # # . . . . . #
行1: . # . # . # . . .
1. 确定计算范围
首先找到st=2(最左边有检测器的位置)和en=9(最右边有检测器的位置)。
2. 初始化dp数组
初始化dp数组为一个很大的值(表示无法到达的状态)。
对于起始位置st=2:
- s[2] [0]=‘#’,所以dp[2] [0]=0(已有检测器,不需要额外花费)
- s[2] [1]=‘#’,所以dp[2] [1]=0(已有检测器,不需要额外花费)
由于第2列的两行都有检测器,它们已经连通,所以dp[2] [0]=dp[2] [1]=0。
3. 逐列计算dp值
对于i=3(第3列):
计算dp[3] [0]:
- s[3] [0]=‘#’(已有检测器)
- 从dp[2] [0]转移:dp[2] [0]=0
- 从dp[2] [1]转移:dp[2] [1]=0,但需要通过(3,1)连接,s[3] [1]=‘.’,需要额外放置一个检测器,所以花费为0+1=1
- 取最小值:dp[3] [0]=min(0,1)=0
计算dp[3] [1]:
- s[3] [1]=‘.’(需要放置检测器)
- 从dp[2] [1]转移:dp[2] [1]=0
- 从dp[2] [0]转移:dp[2] [0]=0,但需要通过(3,0)连接,s[3] [0]=‘#’,不需要额外放置检测器,所以花费为0+0=0
- 由于(3,1)需要放置检测器,额外花费1,所以dp[3] [1]=min(0,0)+1=1
对于i=4(第4列):
计算dp[4] [0]:
- s[4] [0]=‘.’(需要放置检测器)
- 从dp[3] [0]转移:dp[3] [0]=0
- 从dp[3] [1]转移:dp[3] [1]=1,但需要通过(4,1)连接,s[4] [1]=‘#’,不需要额外放置检测器,所以花费为1+0=1
- 由于(4,0)需要放置检测器,额外花费1,所以dp[4] [0]=min(0,1)+1=1
计算dp[4] [1]:
- s[4] [1]=‘#’(已有检测器)
- 从dp[3] [1]转移:dp[3] [1]=1
- 从dp[3] [0]转移:dp[3] [0]=0,但需要通过(4,0)连接,s[4] [0]=‘.’,需要额外放置一个检测器,所以花费为0+1=1
- 取最小值:dp[4] [1]=min(1,1)=1
对于i=5(第5列):
计算dp[5] [0]:
- s[5] [0]=‘.’(需要放置检测器)
- 从dp[4] [0]转移:dp[4] [0]=1
- 从dp[4] [1]转移:dp[4] [1]=1,但需要通过(5,1)连接,s[5] [1]=‘.’,需要额外放置一个检测器,所以花费为1+1=2
- 由于(5,0)需要放置检测器,额外花费1,所以dp[5] [0]=min(1,2)+1=2
计算dp[5] [1]:
- s[5] [1]=‘.’(需要放置检测器)
- 从dp[4] [1]转移:dp[4] [1]=1
- 从dp[4] [0]转移:dp[4] [0]=1,但需要通过(5,0)连接,s[5] [0]=‘.’,需要额外放置一个检测器,所以花费为1+1=2
- 由于(5,1)需要放置检测器,额外花费1,所以dp[5] [1]=min(1,2)+1=2
对于i=6(第6列):
计算dp[6] [0]:
- s[6] [0]=‘.’(需要放置检测器)
- 从dp[5] [0]转移:dp[5] [0]=2
- 从dp[5] [1]转移:dp[5] [1]=2,但需要通过(6,1)连接,s[6] [1]=‘#’,不需要额外放置检测器,所以花费为2+0=2
- 由于(6,0)需要放置检测器,额外花费1,所以dp[6] [0]=min(2,2)+1=3
计算dp[6] [1]:
- s[6] [1]=‘#’(已有检测器)
- 从dp[5] [1]转移:dp[5] [1]=2
- 从dp[5] [0]转移:dp[5] [0]=2,但需要通过(6,0)连接,s[6] [0]=‘.’,需要额外放置一个检测器,所以花费为2+1=3
- 取最小值:dp[6] [1]=min(2,3)=2
对于i=7(第7列):
计算dp[7] [0]:
- s[7] [0]=‘.’(需要放置检测器)
- 从dp[6] [0]转移:dp[6] [0]=3
- 从dp[6] [1]转移:dp[6] [1]=2,但需要通过(7,1)连接,s[7] [1]=‘.’,需要额外放置一个检测器,所以花费为2+1=3
- 由于(7,0)需要放置检测器,额外花费1,所以dp[7] [0]=min(3,3)+1=4
计算dp[7] [1]:
- s[7] [1]=‘.’(需要放置检测器)
- 从dp[6] [1]转移:dp[6] [1]=2
- 从dp[6] [0]转移:dp[6] [0]=3,但需要通过(7,0)连接,s[7] [0]=‘.’,需要额外放置一个检测器,所以花费为3+1=4
- 由于(7,1)需要放置检测器,额外花费1,所以dp[7] [1]=min(2,4)+1=3
对于i=8(第8列):
计算dp[8] [0]:
- s[8] [0]=‘.’(需要放置检测器)
- 从dp[7] [0]转移:dp[7] [0]=4
- 从dp[7] [1]转移:dp[7 ] [1]=3,但需要通过(8,1)连接,s[8] [1]=‘.’,需要额外放置一个检测器,所以花费为3+1=4
- 由于(8,0)需要放置检测器,额外花费1,所以dp[8] [0]=min(4,4)+1=5
计算dp[8] [1]:
- s[8] [1]=‘.’(需要放置检测器)
- 从dp[7] [1]转移:dp[7] [1]=3
- 从dp[7] [0]转移:dp[7] [0]=4,但需要通过(8,0)连接,s[8] [0]=‘.’,需要额外放置一个检测器,所以花费为4+1=5
- 由于(8,1)需要放置检测器,额外花费1,所以dp[8] [1]=min(3,5)+1=4
对于i=9(第9列):
计算dp[9] [0]:
- s[9] [0]=‘#’(已有检测器)
- 从dp[8] [0]转移:dp[8] [0]=5
- 从dp[8] [1]转移:dp[8] [1]=4,但需要通过(9,1)连接,s[9] [1]=‘.’,需要额外放置一个检测器,所以花费为4+1=5
- 取最小值:dp[9] [0]=min(5,5)=5
计算dp[9] [1]:
- s[9] [1]=‘.’(需要放置检测器)
- 从dp[8] [1]转移:dp[8] [1]=4
- 从dp[8] [0]转移:dp[8] [0]=5,但需要通过(9,0)连接,s[9] [0]=‘#’,不需要额外放置检测器,所以花费为5+0=5
- 由于(9,1)需要放置检测器,额外花费1,所以dp[9] [1]=min(4,5)+1=5
4. 计算最终结果
最终答案是min(dp[9] [0], dp[9] [1])=min(5, 5)=5,表示需要增加5个检测器使所有检测器连通。
可视化最终方案
一种可能的最终方案('+'表示新增的检测器):
列号: 1 2 3 4 5 6 7 8 9
行0: . # # + + . . . #
行1: . # . # . # + + .
或者:
列号: 1 2 3 4 5 6 7 8 9
行0: . # # . . . . + #
行1: . # + # + # + + .
这两种方案都需要增加5个检测器,使所有检测器连通。
试题G: 生产车间
问题描述
小明正在改造一个生产车间的生产流水线。这个车间共有n台设备,构成以1为根结点的一棵树,结点i有权值w_i。其中叶节点的权值w_i表示每单位时间将产出w_i单位的材料并送往父结点,根结点的权值w_i表示每单位时间内能打包多少单位成品,其他结点的权值w_i表示每单位时间最多能加工w_i单位的材料并送往父结点。
由于当前生产线中某些结点存在产能不够的问题导致生产线无法正常运行,即存在某些结点每单位时间收到的材料超过了当前结点的加工能力上限。小明计划删除一些结点使得所有结点都能正常运行。他想知道删除一些结点后根结点每单位时间内最多能打包多少单位的成品?
解题思路
这个问题描述了一个生产流水线优化场景:
- 有n台设备构成一棵以1为根的树
- 每个节点有权值w_i,表示其加工能力
- 叶节点产生材料,非叶节点加工材料,根节点打包成品
- 如果节点接收的材料超过其加工能力,生产线无法正常运行
- 可以删除一些节点(及其子树)使所有节点正常运行
- 目标是最大化根节点的产出
关键洞察:这是一个树形结构上的优化问题,我们需要决定保留或删除每个节点,以使根节点的产出最大化。要注意并不是每个节点的最大值组合起来并就一定是所求的最优解,比如样例数据,样例解释见代码后
我们采用自底向上的方法,从叶子节点开始,计算每个节点在不同情况下可能的产出值:
-
对于每个节点,我们有两个基本选择:
- 保留节点:节点产生其权值的产出(对于叶子节点)或处理子节点的材料(对于非叶子节点)
- 删除节点:节点产出为0
-
对于非叶子节点,我们需要:
- 收集所有子节点的可能产出值
- 组合这些产出值,确保总和不超过当前节点的加工能力
- 计算当前节点所有可能的产出值
-
最终,根节点的最大可能产出值就是答案
代码实现
#include <bits/stdc++.h>
using namespace std;
const int MAX_NODES = 1005;
vector<int> g[MAX_NODES]; // 树结构的邻接表
int n, capacity[MAX_NODES]; // 节点数和各节点容量
/**
* 计算以current_node为根的子树能提供的合法材料数值集合
* @param current_node 当前处理的节点
* @param parent_node 父节点(防止回溯)
* @return 包含所有可能值的有序集合
*/
set<int> dfs(int current_node, int parent_node) {
// g[current_node].size() == 1 表示无向图的叶子节点
//parent_node != 0 避免出现链状
if (g[current_node].size() == 1 && parent_node != 0) {
return {capacity[current_node], 0}; // 保留或关闭该节点
}
set<int> valid_sums = {0}; // 初始化,包含0(全关闭情况)
// 使用范围for循环遍历邻接节点
for (int child_node : g[current_node]) {
if (child_node == parent_node) continue; // 跳过父节点
auto child_outputs = dfs(child_node, current_node);//深入遍历子节点
set<int> current_sums = valid_sums; // 当前状态的副本
//依次将任意两个子节点的所有取值进行组合
for (int parent_sum : current_sums) {
for (int child_contribution : child_outputs) {
int total = parent_sum + child_contribution;
if (total <= capacity[current_node]) {
valid_sums.insert(total);
}
}
}
}
return valid_sums;//返回该节点的所有取值情况
}
int main() {
// 输入处理
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> capacity[i];
}
// 构建树结构
for (int i = 1; i < n; ++i) {
int node_a, node_b;
cin >> node_a >> node_b;
g[node_a].push_back(node_b);
g[node_b].push_back(node_a);
}
// 计算并输出最大打包量
cout << *dfs(1, 0).rbegin() << endl;
return 0;
}
注意:这份代码在部分网站测试会有极少量测试集超时,对此可以使用使用位运算bitset代替集合:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MAXW = 1001; // 最大权值+1
int n; // 节点数量
int w[MAXN]; // 每个节点的权值(容量)
vector<int> graph[MAXN]; // 无向图邻接表
/**
* 计算以u为根的子树能提供的合法材料数值集合
* 使用bitset优化状态表示和组合操作
* @param u 当前处理的节点
* @param parent 父节点(防止回溯)
* @return 包含所有可能值的位向量
*/
bitset<MAXW> dfs(int u, int parent) {
// 判断是否为叶子节点
if (graph[u].size() == 1 && parent != 0) {
bitset<MAXW> result;
result[0] = 1; // 可以产出0(删除节点)
result[w[u]] = 1; // 可以产出w[u](保留节点)
return result;
}
// 初始化结果位向量,只有0位为1(表示所有子节点都关闭的情况)
bitset<MAXW> result;
result[0] = 1;
// 遍历所有子节点
for (int v : graph[u]) {
if (v == parent) continue; // 跳过父节点
// 递归计算子节点的所有可能产出
bitset<MAXW> child_outputs = dfs(v, u);
// 保存当前结果的副本
bitset<MAXW> current = result;
result.reset(); // 清空结果
// 优化的组合操作
for (int i = 0; i <= w[u]; i++) {
if (current[i]) {
// 对于当前值i,与子节点的每个可能产出组合
for (int j = 0; j <= w[u] - i; j++) {
if (child_outputs[j]) {
result[i + j] = 1;
}
}
}
}
}
return result; // 返回所有可能的产出值
}
int main() {
ios_base::sync_with_stdio(false); // 优化输入输出
cin.tie(nullptr);
cin >> n;
// 读取每个节点的权值
for (int i = 1; i <= n; i++) {
cin >> w[i];
}
// 读取树的边
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
// 计算根节点的所有可能产出
bitset<MAXW> root_outputs = dfs(1, 0);
// 找出最大可能产出
int ans = 0;
for (int i = MAXW - 1; i >= 0; i--) {
if (root_outputs[i]) {
ans = i;
break;
}
}
cout << ans << endl;
return 0;
}
样例详细阐述
让我们以给定的样例来详细说明算法的执行过程:
9
9 7 3 7 1 6 2 2 7
1 2
1 3
2 4
2 5
2 6
6 7
6 8
6 9
树的结构如下:
1
/ \
2 3
/|\
4 5 6
/|\
7 8 9
节点权值:w = [9, 7, 3, 7, 1, 6, 2, 2, 7](索引从1开始)
执行DFS过程:
-
叶子节点:
- 节点3:返回3, 0
- 节点4:返回7, 0
- 节点5:返回1, 0
- 节点7:返回2, 0
- 节点8:返回2, 0
- 节点9:返回7, 0
-
节点6(容量为6):
-
初始结果集:0
-
处理子节点7(2, 0):结果集变为0, 2
-
处理子节点8(2, 0):结果集变为0, 2, 4
-
处理子节点9(7, 0):
-
0 + 0 = 0,已在结果集中
-
0 + 7 = 7 > 6,超过容量,不添加
-
2 + 0 = 2,已在结果集中
-
2 + 7 = 9 > 6,超过容量,不添加
-
4 + 0 = 4,添加到结果集
-
4 + 7 = 11 > 6,超过容量,不添加
最终结果集:0, 2, 4
-
-
节点2(容量为7):
-
初始结果集:0
-
处理子节点4(7, 0):结果集变为0, 7
-
处理子节点5(1, 0):结果集变为0, 1, 7, 8,但8 > 7,所以实际为0, 1, 7
-
处理子节点6(0, 2, 4):
-
组合后得到0, 1, 2, 4, 7, 3 (2 + 1), 9 (2 + 7 应舍去), 5(4 + 1), 11(4 + 7 应舍去)
-
最终结果集:0, 1, 2, 3, 4, 5, 7
- 根节点1(容量为9):
-
初始结果集:0
-
处理子节点2(0, 1, 2, 3, 4, 5, 7):结果集变为0, 1, 2, 3, 4, 5, 7
-
处理子节点3(3, 0):
-
组合后得到0, 1, 2, 3, 4, 5, 6 (3 + 3), 7 (4 + 3), 8 (5 + 3),10 (7 + 1),但10 > 9,所以实际为0, 1, 2, 3, 4, 5, 6, 7, 8
-
最终结果集:0, 1, 2, 3, 4, 5, 6, 7, 8
- 最终答案:根节点1的结果集中的最大值为8
试题H: 装修报价
问题描述
老王计划装修房子,联系了一家装修公司。该公司有一套自动报价系统,只需用户提供N项装修相关费用A₁,A₂,…,A_N,系统便会根据这些费用生成最终的报价。
系统会依据某种内部算法,在每对相邻数字之间插入+(加法)、−(减法)或⊕(异或)运算符,并按照特定优先级规则计算结果:异或运算优先级最高,其次是加减。
老王决定模拟其运作方式,尝试每种可能的运算符组合,计算出所有可能出现的结果的总和。请你帮老王算出所有可能的结果的总和。由于该总和可能很大,你只需提供其对10^9+7取余后的结果即可。
方法一:前缀异或枚举法
解题思路
核心思想是枚举前缀异或的长度,计算每个前缀异或在所有可能组合中的贡献,然后求和。
关键洞察:
- 考虑给表达式的第一个值前面也补上+号,例如:2−3⊕4 实际上是 +2−3⊕4
- 由于异或运算的优先级高于加减法,连续的异或运算结果可以合并成一个数字,使表达式变成只包含+和-的形式
- 所有的"+XXX"和"-XXX"在不同的式子中会同时出现,它们的贡献会相互抵消,也就是:对组合的式子分割为A+B,那么一定存在A-B的组合情况,这两者相加就会使得B相互抵消了,仅有A有贡献值
- 对最终结果有贡献的只有前缀异或
对于长度为i的前缀异或S[i](即A[1] ⊕ A[2] ⊕ … ⊕ A[i]),其贡献计算方式为:
- 第i+1个位置的运算符必须是+或-(2种选择,即把前面i个运算符的结果看作是一个数字A,后面的运算看作一个数字B),不能是异或(A + B / A - B)
- 其中,第i+2到第n个位置的运算符可以是任意三种(3^(n-i-1)种选择,无论是哪一种都有对应的组合将其抵消掉)
- 因此,贡献为:S[i] * 2 * 3^(n-i-1)
**特殊情况:**当i=n时(即所有数字都异或),贡献为S[i]。
最后的总结果就是所有前缀异或的加权和:
结果 = S_1 * 2 * 3^(n-2) + S_2 * 2 * 3^(n-3) + ... + S_(n-1) * 2 * 3^0 + S_n * 1
代码实现
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
vector<int> a;
// 优化计算a^b % MOD
ll cal(ll a, ll b) {
ll res = 1;
while (b > 0) {
//b是奇数,先乘掉一个a
if (b % 2 == 1) res = (res * a) % MOD;
//a^10 = (a ^ 2) ^ 5从而降低循环次数
a = (a * a) % MOD;
//指数b /= 2
b /= 2;
}
return res;
}
int main() {
int n;
cin >> n;
// 只有一个数字
if (n == 1) {
int x;
cin >> x;
cout << x << endl;//直接输出
return 0;
}
a.resize(n + 1);//存储n个数字
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
ll ans = 0;
ll s = 0;//存储异或值(即a[1] ⊕ a[2] ⊕ ... ⊕ a[i])
for (int i = 1; i <= n; i++) {
s ^= a[i];
if (i < n) {
// 不是最后一个数字:贡献为 s * 2 * 3^(n-i-1)
ll cnt = (s * 2) % MOD;
cnt = (cnt * cal(3, n - i - 1)) % MOD;
ans = (ans + cnt) % MOD;
} else {
// 是最后一个数字:贡献为 s
ans = (ans + s) % MOD;
}
}
cout << ans << endl;
return 0;
}
样例分析
以样例 [0, 2, 5]
为例,详细演示算法执行过程:
初始状态:
- n = 3
- A = [0, 0, 2, 5](代码执行时索引从1开始)
- ans = 0
- s= 0
第1次迭代 (i=1, A[1]=0):
- s= 0 ^ 0 = 0
- i < n,计算贡献:
- cnt = 0 * 2 * 3^(3-1-1) = 0 * 2 * 3^1 = 0 * 2 * 3 = 0
- ans = 0 + 0 = 0
第2次迭代 (i=2, A[2]=2):
-
s = 0 ^ 2 = 2
-
i < n,计算贡献:
-
cnt = 2 * 2 * 3^(3-2-1) = 2 * 2 * 3^0 = 2 * 2 * 1 = 4
-
ans = 0 + 4 = 4
第3次迭代 (i=3, A[3]=5):
-
s = 2 ^ 5 = 7
-
i = n,计算贡献:
-
cnt = 7
-
ans = 4 + 7 = 11
最终答案为11,与样例一致。
方法二:递推关系法
解题思路
核心思想是根据方法一原理,利用递推关系直接计算所有可能结果的总和,避免枚举所有3^(N-1)种组合。
关键洞察:
-
对于除第一项外的每一项,其贡献总和为0。这是因为对于任意非第一项的数字A[i],如果我们将其前面的+改为-(或将-改为+),而保持其他运算符不变,这两种情况的结果会互为相反数,在总和中相互抵消。
- 例如数据a,b,c : a + b + c; a + b - c; a + b ⊕ c 我们可以用a - b + c; a - b - c; a - b ⊕ c 完全消去b的贡献
-
唯一有贡献的是从第一项开始的连续异或序列(前缀异或)。
基于这一洞察,我们可以推导出递推关系:
- 设S[k]表示前k个数的异或和:S[k] = A[1] ⊕ A[2] ⊕ … ⊕ A[k]
- 设Ans[k]表示考虑前k个数字时所有可能结果的总和
递推关系为:
Ans[k] = 3 * Ans[k-1] - S[k-1] + S[k]
这个递推关系可以这样理解:
- 当考虑第k个数字时,该数字前的符号有3种可能(添加+、-或⊕),所以乘以3
- 需要减去前k-1个数字的异或和S[k-1]的贡献,因为在计算3*Ans[k-1]时重复计算了
- 需要加上包含第k个数字的新异或和,S[k]的贡献
代码解释
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
int main() {
int n;
cin >> n;
ll ans = 0; // 总答案
ll s = 0; // 当前前缀异或和
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
// 更新答案:ans = ans * 3 - s + (s ^ x)
ans = (ans * 3 - s + (s ^ x) + MOD) % MOD;
// 更新前缀异或和
s ^= x;
}
cout << ans << endl;
return 0;
}