洛谷刷题6.22&&6.21

P2910 [USACO08OPEN] Clear And Present Danger S

代码思路总结(基于洛谷 P2910 问题)

该代码解决的是 图上的最短路径累积问题:给定一个有向图(或无向图,代码中未区分)和一系列必须按顺序访问的点,计算从序列起点到终点依次访问相邻点所走的最短路径总长度。

核心步骤:
  1. 输入处理

    • 读入点数 n 和访问序列长度 m
    • 读入长度为 m 的访问序列 v[]v[1] 是起点,v[m] 是终点)。
    • 读入 n × n 邻接矩阵 dp[][],其中 dp[i][j] 表示点 i 到点 j 的直接距离(无边时需根据题目设定,代码中未显式处理无穷大)。
  2. 计算所有点对的最短路径

    • 使用 Floyd-Warshall 算法(三层循环 k, i, j)动态更新最短路径:
      for (int k = 1; k <= n; k++)
          for (int i = 1; i <= n; i++)
              for (int j = 1; j <= n; j++)
                  dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
      
    • 结束后,dp[i][j] 存储点 i 到点 j 的全局最短路径长度。
  3. 累加访问序列的路径长度

    • 遍历访问序列 v[],对每一对相邻点 (v[i-1], v[i]),查询其最短路径并累加:
      ll ans = 0;
      for (int i = 2; i <= m; i++) // 从序列的第2个点开始
          ans += dp[v[i-1]][v[i]];
      
    • 总路径 = dist(v[1]→v[2]) + dist(v[2]→v[3]) + ... + dist(v[m-1]→v[m])
  4. 输出结果:打印累加值 ans

关键特点:
  • Floyd 算法的适用性:适用于稠密图(n ≤ 100),时间复杂度为 O(n³),空间复杂度 O(n²)。
  • 序列访问:无需考虑序列点之间的其他路径,直接利用预计算的最短路径查询。
  • 题目背景:对应洛谷 P2910(“Clear And Present Danger”),本质是求指定路径序列的最短距离累积。
改进建议:
  • 若图稀疏或 n 较大,可改用 Dijkstra 算法(但本题 n=100 适用 Floyd)。
  • 邻接矩阵初始化时,需确保自环为 0dp[i][i]=0),无边用较大值(如 0x3f3f3f3f)表示,代码中未显式处理需注意输入数据。

此方案高效利用了 Floyd 算法的全源最短路特性,将问题转化为简单的序列查询。

完整代码

#include
#define ll long long 
using namespace std;
ll n,m,dp[105][105],v[10005];
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
	cin>>v[i];
for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
		cin>>dp[i][j];
for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
ll ans=0;
for(int i=2;i<=m;i++)
	ans+=dp[v[i-1]][v[i]];
cout<<ans;
	return 0;
}

P5322 [BJOI2019] 排兵布阵

代码思路总结(洛谷 P5322 [BJOI2019] 排兵布阵)

该代码解决的是 分组背包问题,结合了贪心排序思想。核心目标是在有限的士兵数量下,通过合理分配兵力到不同城堡,最大化总得分。得分规则为:在每个城堡中,若你的兵力严格大于某对手兵力的两倍,则战胜该对手得 1 分。

核心步骤:
  1. 输入处理

    • 读入对手数 s、城堡数 n、士兵总数 m
    • 读入 n × s 矩阵 arr,其中 arr[j][i] 表示第 j 个城堡中第 i 个对手的兵力。
      for (int i = 1; i <= s; i++)
          for (int j = 1; j <= n; j++)
              cin >> arr[j][i];  // 城堡 j 的对手 i 的兵力
      
  2. 贪心预处理

    • 每个城堡的对手兵力排序(从小到大):
      for (int i = 1; i <= n; i++)
          sort(arr[i] + 1, arr[i] + 1 + s);
      
    • 排序后,arr[i][k] 表示城堡 i 中第 k 小的对手兵力。此时若在城堡 i 派出 2 * arr[i][k] + 1 兵力,可至少战胜前 k 个对手(因为他们的兵力均 ≤ arr[i][k])。
  3. 分组背包 DP

    • 状态定义dp[j] 表示使用 j 个士兵能获得的最大总得分。
    • 背包分组:每个城堡视为一组物品,每组包含 s 个物品:
      • 物品 k:战胜前 k 个对手
      • 代价2 * arr[i][k] + 1(所需最小兵力)
      • 价值k * i(战胜 k 人 × 城堡编号 i 的得分系数)
    • 状态转移(倒序枚举容量避免重复):
      for (int i = 1; i <= n; i++) {          // 枚举城堡(分组)
          for (int j = m; j >= 0; j--) {       // 倒序枚举士兵数
              for (int k = 1; k <= s; k++) {   // 枚举战胜对手数
                  int cost = 2 * arr[i][k] + 1;
                  if (j >= cost) {
                      dp[j] = max(dp[j], dp[j - cost] + k * i);
                  }
              }
          }
      }
      
  4. 输出结果

    • 遍历所有可能的士兵消耗量(0m),取最大得分:
      ll ans = 0;
      for (int i = 0; i <= m; i++) 
          ans = max(ans, dp[i]);
      cout << ans;
      
关键思想:
  1. 贪心排序

    • 对每个城堡的对手兵力排序,确保用最小代价战胜尽可能多的对手。
    • 性质:战胜前 k 个对手的代价仅取决于第 k 小的兵力(2 * arr[i][k] + 1)。
  2. 分组背包模型

    • 分组:每个城堡是一组独立的决策,只能选择一种策略(战胜 k 人或零)。
    • 物品属性:代价与价值取决于城堡编号 i 和战胜人数 k
    • 倒序枚举:确保每组内仅选一个物品(类似 01 背包)。
  3. 时间复杂度:O(n × m × s),满足题目约束(n, s ≤ 100,m ≤ 2e4)。

改进建议:
  • 边界处理:可显式初始化 dp[0]=0,但代码中默认全 0 已隐含此状态。
  • 空间优化:使用一维 DP 数组是标准的分组背包空间优化方式。

此解法通过贪心预处理转化问题,再套用分组背包框架,高效解决了兵力分配问题。

完整代码

#include
#define ll long long 
using namespace std;
ll s,n,m,dp[20005],arr[105][105];
int main()
{
cin>>s>>n>>m;
for(int i=1;i<=s;i++)
	for(int j=1;j<=n;j++)
		cin>>arr[j][i];
for(int i=1;i<=n;i++)
	sort(arr[i]+1,arr[i]+1+s);
for(int i=1;i<=n;i++){
	for(int j=m;j>=0;j--){
		for(int k=1;k<=s;k++){
			if(j-2*arr[i][k]-1>=0)
				dp[j]=max(dp[j],dp[j-2*arr[i][k]-1]+k*i);
		}
	}
}	
ll ans=0;
for(int i=1;i<=m;i++)
	ans=max(ans,dp[i]);
cout<<ans;
	return 0;
}

P1782 旅行商的背包

代码思路总结(洛谷 P1782 旅行者的背包)

该代码解决的是 多重背包与特殊物品组合优化问题。问题分为两部分:处理普通物品(多重背包)和处理特殊物品(具有非线性收益的背包问题)。代码核心是动态规划,结合了二进制优化和启发式优化策略。

核心步骤:
  1. 输入处理

    • 读入普通物品数 n、特殊物品数 m、背包容量 v
    • 对于每个普通物品,读入体积 a、价值 b 和数量 c
  2. 普通物品处理(二进制优化的多重背包)

    • 使用二进制拆分将每个物品拆分为 2 的幂次份(如 1, 2, 4, …)。
    • 对拆分后的物品执行 01 背包(倒序枚举容量):
      int temp = 1;
      while (temp < c) {
          c -= temp;
          for (int i = v; i >= temp * a; i--) {
              dp[i] = max(dp[i], dp[i - temp * a] + temp * b);
          }
          temp *= 2;
      }
      // 处理剩余部分
      for (int i = v; i >= c * a; i--) {
          dp[i] = max(dp[i], dp[i - c * a] + c * b);
      }
      
  3. 特殊物品处理(非线性收益优化)

    • 每个特殊物品的收益函数为二次函数:g(j) = a*j*j + b*j + cj 是分配给该物品的容量)。
    • 采用启发式优化策略,对于每个容量 i,仅在关键点枚举 j
      • 端点:j = 0j = i
      • 二次函数极值点附近:计算极值点 j0 = -b/(2a),取 j0 前后一定范围(如 200 个点)。
    • 更新状态:
      vector<ll> new_dp = dp;  // 保存上一状态
      for (int i = 0; i <= v; i++) {
          vector<int> candidates = {0, i};  // 端点
          if (a != 0) {
              double j0_val = -b / (2.0 * a);
              int j0_floor = floor(j0_val), j0_ceil = ceil(j0_val);
              // 极值点附近采样
              for (int j = j0_floor - 200; j <= j0_floor + 200; j++) 
                  if (j >= 0 && j <= i) candidates.push_back(j);
              for (int j = j0_ceil - 200; j <= j0_ceil + 200; j++) 
                  if (j >= 0 && j <= i) candidates.push_back(j);
          }
          // 去重并枚举候选点
          sort(candidates.begin(), candidates.end());
          auto last = unique(candidates.begin(), candidates.end());
          candidates.erase(last, candidates.end());
          for (int j : candidates) {
              new_dp[i] = max(new_dp[i], dp[i - j] + a*j*j + b*j + c);
          }
      }
      dp = new_dp;  // 更新状态
      
  4. 输出结果:打印 dp[v](背包容量 v 下的最大收益)。

关键思想:
  1. 二进制优化

    • 将多重背包转化为多个 01 背包,降低时间复杂度。
    • 时间复杂度:普通物品处理为 O(n * v * log c)。
  2. 特殊物品的启发式优化

    • 二次函数性质:收益函数 g(j) 的极值点在 j0 = -b/(2a) 附近。
    • 关键点采样:仅在极值点附近和端点采样,减少枚举量(常数级别)。
    • 时间复杂度:特殊物品处理为 O(m * v * K),其中 K 是采样点数(约 400),总复杂度约 O(100 * 10000 * 400) = 4e8,可接受。
改进建议:
  • 采样范围调整:根据数据特性调整极值点附近的采样范围(如 200)。
  • 线性函数特判:当 a = 0 时(线性函数),仅需枚举端点。
  • 空间优化:使用一维 DP 数组,避免额外空间开销。

该解法通过二进制优化处理普通物品,结合二次函数性质和关键点采样高效处理特殊物品,在较大规模数据下表现良好。

完整代码

#include
#define ll long long 
using namespace std;
ll n,m,v,a,b,c,dp[10005];
int main()
{
cin>>n>>m>>v;
while(n--){
	cin>>a>>b>>c;
	int temp=1;
	while(temp<c){
		c-=temp;
		for(int i=v;i>=temp*a;i--){
			dp[i]=max(dp[i],dp[i-temp*a]+temp*b);
		}
		temp*=2;
	}
	for(int i=v;i>=c*a;i--){
		dp[i]=max(dp[i],dp[i-c*a]+c*b);
	}	
}
while(m--){
	cin>>a>>b>>c;
	for(int i=v;i>=0;i--){
		for(int j=0;j<=i;j++){
			dp[i]=max(dp[i],dp[i-j]+a*j*j+b*j+c);
		}
	}
}
cout<<dp[v];
	return 0;
}

P2851 [USACO06DEC] The Fewest Coins G

代码思路总结(洛谷 P2851 [USACO06DEC] The Fewest Coins G)

该代码解决的是 货币支付问题:John 需要支付 m 元给商家,他有 n 种面值的硬币(数量有限),商家有无限量的硬币可以找零。目标是最小化 John 经手的总硬币数(支付硬币数 + 找回硬币数)。

核心思路:
  1. 关键结论

    • 最优支付金额 P 满足:m ≤ P ≤ m + maxv - 1maxv 是最大面值,≤120)
    • 因此只需枚举支付金额 P[m, m+120] 范围内
  2. 多重背包(John 的支付方案)

    • 状态定义:dp[j] 表示 John 恰好支付 j 元所需的最小硬币数
    • 二进制优化:
      • 对每种硬币进行二进制拆分(1, 2, 4, …)
      • 倒序枚举金额(01 背包方式更新):
        int temp = 1;
        while (temp < c[i]) {
            c[i] -= temp;
            for (j = m+120; j >= temp*v[i]; j--)
                dp[j] = min(dp[j], dp[j-temp*v[i]] + temp);
            temp *= 2;
        }
        for (j = m+120; j >= c[i]*v[i]; j--)
            dp[j] = min(dp[j], dp[j-c[i]*v[i]] + c[i]);
        
  3. 完全背包(商家的找零方案)

    • 对每个枚举的支付金额 Pm ≤ P ≤ m+120):
      • 计算找零金额 sum = P - m
      • 动态规划计算找零的最小硬币数:
        int arr[sum+1]; // arr[k] 表示找零 k 元的最小硬币数
        arr[0] = 0;
        for (int k = 1; k <= sum; k++) arr[k] = INF;
        for (int i = 1; i <= n; i++)
            for (int j = v[i]; j <= sum; j++)
                arr[j] = min(arr[j], arr[j-v[i]] + 1);
        
  4. 更新答案

    • dp[P]arr[sum] 均有效:
      ans = min(ans, dp[P] + arr[sum]);
      
算法特点:
  1. 双重背包设计

    • John 的支付:多重背包(二进制优化)
    • 商家的找零:完全背包(现场计算)
  2. 复杂度优化

    • 利用结论将枚举范围从 [m, m+maxv^2] 缩小到 [m, m+120]
    • 多重背包二进制优化:O(n × (m+120) × log c)
    • 完全背包:O(121 × n × 120) ≈ 1.5e6
  3. 关键实现细节

    • 初始化:dp[0]=0,其他为极大值(11005)
    • 找零 DP 每次独立计算(因 sum 较小)
    • 倒序更新避免硬币重复使用
正确性证明:
  • 支付金额范围:若 P ≥ m + maxv,可通过减少一枚面值 ≤ maxv 的硬币使总硬币数减少,矛盾
  • 边界处理:当 sum=0(不需找零)时,arr[0]=0 保证正确性

该解法高效结合了背包问题的优化技巧,充分利用了问题特性,在较大数据规模下仍保持良好性能。

完整代码

#include
#define ll long long 
using namespace std;
int n,m,dp[11005],v[105],c[105],ans=11005; 
bool flag=false;
void suan(int sum){
	int arr[sum+1];
	arr[0]=0;
	for(int i=1;i<=sum;i++)
		arr[i]=11005;
	for(int i=1;i<=n;i++)
		for(int j=v[i];j<=sum;j++)
			arr[j]=min(arr[j],arr[j-v[i]]+1);
	if(arr[sum]!=11005){
		flag=true;
		ans=min(ans,arr[sum]+dp[sum+m]);
	}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
	cin>>v[i];
for(int i=1;i<=n;i++)
	cin>>c[i];
for(int i=1;i<=m+120;i++)
	dp[i]=11005;
for(int i=1;i<=n;i++){
	int temp=1;
	while(c[i]>temp){
		c[i]-=temp;
		for(int j=m+120;j>=temp*v[i];j--)
			dp[j]=min(dp[j],dp[j-temp*v[i]]+temp);		
		temp*=2;
	}
	for(int j=m+120;j>=c[i]*v[i];j--)
		dp[j]=min(dp[j],dp[j-c[i]*v[i]]+c[i]);
}
for(int i=m;i<=m+120;i++){
	if(dp[i]<ans){
		suan(i-m);
	}
}				
if(flag) cout<<ans;
else cout<<"-1";
	return 0;
}

P1110 [ZJOI2007] 报表统计

代码思路总结(洛谷 P1110 [ZJOI2007] 报表统计)

该代码解决的是 多限制条件下的物品选择问题:从 n 种物品中选择若干种,每种物品有三个属性 a, b, c。要求选出的物品满足:

  1. a 属性之和 ≥ m
  2. b 属性之和 ≥ r
  3. 最大化选出的物品种类数 j
  4. 在满足最大 j 的前提下,最小化 c 属性之和
核心思路:
  1. 状态定义

    • dp[j][k][p] 表示选择 j 种物品时,a 属性之和为 kb 属性之和为 p 的最小 c 属性之和
    • 数组范围:j ∈ [0, n], k ∈ [0, 100], p ∈ [0, 100](隐含要求 m, r ≤ 100
  2. 初始化

    • dp[0][0][0] = 0(未选择任何物品)
    • 其他状态初始化为极大值 0x3f3f3f3f
  3. 动态规划(分组背包)

    • 对每种物品 i 进行逆序枚举:
      • 枚举物品种类数 j:从 i 递减到 1
      • 枚举 a 属性之和 k:从 m 递减到 a[i]
      • 枚举 b 属性之和 p:从 r 递减到 b[i]
      • 状态转移方程:
        dp[j][k][p] = min(dp[j][k][p], dp[j-1][k-a[i]][p-b[i]] + c[i])
        
  4. 更新答案(存在缺陷)

    • 在状态转移过程中实时更新最优解:
      • dp[j][k][p] 有效(非无穷大):
        • 若当前 j 大于历史最大种类数 now,则更新 now = jans = dp[j][k][p]
        • j 等于 now,则更新 ans = min(ans, dp[j][k][p])
    • 缺陷:未检查约束条件 k ≥ mp ≥ r
  5. 输出结果

    • 若找到有效解 (flag = true),输出最小 cans
    • 否则输出 -1
关键特点:
  1. 三维状态设计

    • 第一维:选出的物品种类数 j
    • 第二维:a 属性累积和 k
    • 第三维:b 属性累积和 p
  2. 逆序枚举

    • 通过倒序枚举 j, k, p 实现分组背包,避免物品重复选择
  3. 局限性

    • 数组大小固定为 [101][101][101],隐含要求 m, r ≤ 100
    • 状态转移中未验证 k ≥ mp ≥ r,可能导致无效解被更新
改进建议:
  1. 约束条件检查

    • 应在更新最优解时增加约束验证:
      if (dp[j][k][p] != INF && k >= m && p >= r) {
          // 更新 now 和 ans
      }
      
  2. 最终扫描优化

    • 改为在所有 DP 结束后扫描有效解:
      int now = 0, ans = INF;
      for (int j = 0; j <= n; j++) {
          for (int k = m; k <= 100; k++) {
              for (int p = r; p <= 100; p++) {
                  if (dp[j][k][p] < INF) {
                      if (j > now) {
                          now = j;
                          ans = dp[j][k][p];
                      } else if (j == now) {
                          ans = min(ans, dp[j][k][p]);
                      }
                  }
              }
          }
      }
      
  3. 大范围数据支持

    • m, r > 100,需修改状态设计:
      • 将超过阈值部分压缩存储(如 k = min(k, m)
      • 或使用更高效的算法(如二维费用背包 + 极值压缩)
算法复杂度:
  • 时间复杂度 O ( n 2 ⋅ m ⋅ r ) O(n^2 \cdot m \cdot r) O(n2mr)
  • 空间复杂度 O ( n ⋅ m ⋅ r ) O(n \cdot m \cdot r) O(nmr)
  • n, m, r ≤ 100 时可行,更大数据需优化

注意:此解法针对原题小数据范围设计,若 m, r > 100 需使用其他算法(如基于物品数量的分层 DP + 状态压缩)

完整代码

#include
#define ll long long 
using namespace std;
int dp[101][101][101],n,a[101],b[101],c[101],m,r,now=0,ans=0;
int main()
{
cin>>n;
for(int i=1;i<=n;i++){
	cin>>a[i]>>b[i]>>c[i];
}
cin>>m>>r;
memset(dp,0x3f,sizeof(dp));
dp[0][0][0]=0;
for(int i=1;i<=n;i++){
	for(int j=i;j>=1;j--){
		for(int k=m;k>=a[i];k--){
			for(int p=r;p>=b[i];p--){
				dp[j][k][p]=min(dp[j][k][p],dp[j-1][k-a[i]][p-b[i]]+c[i]);
				if(dp[j][k][p]!=0x3f3f3f3f){
					if(j==now){
						ans=min(ans,dp[j][k][p]);
					}
					else if(j>now){
						now=j;
						ans=dp[j][k][p];
					}
				}
			}
		}
	}
}
cout<<ans;
	return 0;
}

P1561 [USACO12JAN] Mountain Climbing S

代码思路总结(洛谷 P1561 [USACO05JAN] Mountain Climbing)

该代码解决的是 双阶段任务调度问题:有 n 头牛需要依次完成上山和下山任务,每头牛的上山时间为 up,下山时间为 down。关键约束是:当一头牛开始下山时,下一头牛可以同时开始上山(即上山与下山阶段可以重叠)。目标是最小化所有牛完成任务的总时间。

核心思路:贪心排序 + 时序模拟
  1. 贪心排序策略

    • 将牛分为两类:
      • 第一类up ≤ down(上山快下山慢)
      • 第二类up > down(上山慢下山快)
    • 排序规则
      • 所有第一类牛排在第二类牛之前
      • 第一类牛内部:按 up 升序排列(上山时间短的优先)
      • 第二类牛内部:按 down 降序排列(下山时间长的优先)
    • 比较函数实现:
      bool cmp(point &s1, point &s2) {
          if (s1.up < s1.down) {        // s1是第一类
              if (s2.up < s2.down)      // s2是第一类
                  return s1.up < s2.up; // 按up升序
              return true;              // 第一类排第二类前
          } else {                      // s1是第二类
              if (s2.up > s2.down)      // s2是第二类
                  return s1.down > s2.down; // 按down降序
              return false;             // 第二类排第一类后
          }
      }
      
  2. 时序模拟

    • 初始化
      • uptime = 0(累计上山时间)
      • lastend = 0(最后一头牛下山结束时间)
    • 遍历排序后的牛
      for (int i = 1; i <= n; i++) {
          uptime += arr[i].up;  // 累加上山时间
          // 更新下山结束时间:
          // - 下山开始时间 = max(上一头牛的下山结束时间, 当前牛上山结束时间)
          // - 下山结束时间 = 下山开始时间 + 下山耗时
          lastend = max(lastend, uptime) + arr[i].down;
      }
      
    • 输出结果lastend 即最小总时间
算法正确性证明:
  1. 第一类牛(上山快)优先处理

    • 上山时间短的任务先完成,尽早开始下山(因下山耗时较长)
    • 避免后续任务因等待下山而延迟
  2. 第二类牛(下山慢)前置处理

    • 下山时间长的任务优先处理,利用后续任务的上山时间重叠其下山过程
    • 例:下山耗时长的牛先下山时,后续牛可同时上山,减少总等待时间
  3. 重叠利用关键

    • lastend = max(lastend, uptime) + down 精确建模了任务重叠:
      • lastend > uptime:下山连续进行(无空闲)
      • uptime > lastend:上山结束后立即开始下山
复杂度分析:
  • 时间复杂度:O(n log n)(排序主导)
  • 空间复杂度:O(n)(存储任务数据)
示例解析:

假设两头牛:

  • 牛A:up=1, down=100(第一类)
  • 牛B:up=100, down=1(第二类)

按贪心排序:牛A → 牛B

  • 牛A:uptime=1, lastend = max(0,1)+100=101
  • 牛B:uptime=1+100=101, lastend = max(101,101)+1=102

若逆序(牛B → 牛A):

  • 牛B:uptime=100, lastend=100+1=101
  • 牛A:uptime=100+1=101, lastend = max(101,101)+100=201(更差)

关键洞察:通过合理排序,使耗时长的下山阶段尽可能与后续上山阶段重叠,最大化利用并行性。

完整代码

#include
#define ll long long 
using namespace std;
struct point{
	int up,down;
}arr[50005]; 
int n;
bool cmp(point &s1,point &s2){
	if(s1.up<s1.down){
		if(s2.up<s2.down){
			return s1.up<s2.up;
		}
		return true;
	}
	else{
		if(s2.up>s2.down){
			return s1.down>s2.down;
		}
		return false;
	}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
cin>>n;
for(int i=1;i<=n;i++){
	cin>>arr[i].up>>arr[i].down;
}
sort(arr+1,arr+1+n,cmp);
int uptime=0,lastend=0;
for(int i=1;i<=n;i++){
	uptime+=arr[i].up;
	lastend=max(lastend,uptime)+arr[i].down;
}
cout<<lastend;
	return 0;
}

P6327 区间加区间 sin 和

代码思路总结(洛谷 P6327 [COCI2006-2007#4] IZBORI)

该代码解决的是 区间修改与三角函数和查询问题:维护一个整数序列,支持两种操作:

  1. 区间加法:给区间 [l, r] 的所有数加 k
  2. 区间查询:计算区间 [l, r] 所有数的正弦值之和(结果四舍五入保留一位小数)
核心思路:线段树 + 三角函数性质
  1. 数据结构设计

    • 线段树节点
      • l, r:节点覆盖的区间
      • lz:区间加法懒标记(累计未下传的增量)
      • sums:区间内所有数的正弦值之和
      • sumc:区间内所有数的余弦值之和
  2. 关键数学原理(和角公式):

    • 当数 a 增加 k 时:
      • sin(a+k) = sin(a)cos(k) + cos(a)sin(k)
      • cos(a+k) = cos(a)cos(k) - sin(a)sin(k)
    • 对区间整体加 k
      • 新正弦和 = 原正弦和 × cos(k) + 原余弦和 × sin(k)
      • 新余弦和 = 原余弦和 × cos(k) - 原正弦和 × sin(k)
  3. 核心操作

    • 建树
      • 叶子节点:存储初始值的正弦和余弦
      • 非叶子节点:合并子节点的正弦和与余弦和
    • 懒标记下传
      • 用临时变量保存子节点原始的正弦和余弦值
      • 通过和角公式更新子节点的 sumssumc
      • 累加懒标记到子节点
    • 区间加法
      • 完全覆盖:直接更新当前节点的 sumssumc,累加懒标记
      • 部分覆盖:下传懒标记后递归处理子区间,回溯时合并信息
    • 区间查询
      • 完全覆盖:直接返回 sums
      • 部分覆盖:下传懒标记后递归查询子区间并求和
  4. 结果处理

    • 对查询结果乘以10后四舍五入,再除以10得到保留一位小数的结果:
      round(result * 10) / 10
      
算法特点:
  1. 高效区间维护

    • 利用线段树在 O(log n) 时间内完成区间操作
    • 懒标记机制延迟更新,提高效率
  2. 数学性质应用

    • 通过和角公式将加法操作转化为线性变换
    • 维护余弦和辅助正弦和更新
  3. 精度处理

    • 显式四舍五入避免浮点误差
    • 输出保留一位小数
复杂度分析:
  • 时间复杂度
    • 建树:O(n)
    • 区间操作/查询:O(log n)
  • 空间复杂度:O(n)
改进建议:
  1. 懒标记优化

    • 预处理 sin(k)cos(k) 避免重复计算
    • 使用弧度制减少三角函数计算量
  2. 查询优化

    • 添加查询缓存机制减少重复计算
  3. 精度增强

    • 使用更高精度浮点数(如 long double)减少累积误差

关键洞察:将整数加法转化为三角函数的线性变换,利用线段树高效维护区间和,通过和角公式保证更新的正确性。

完整代码

#include
#define ll long long 
using namespace std;
struct node{
	int r,l;
	ll lz;
	double sums,sumc;
}tree[820020];
int a,n,m;
double s[200005],c[200005];
void build(int i,int l,int r){//建树 
	tree[i].l=l,tree[i].r=r,tree[i].lz=0;
	if(l==r){
		tree[i].sums=s[l];
		tree[i].sumc=c[l];
		return;
	}
	int mid=(l+r)/2;
	build(2*i,l,mid);//递归左右子树 
	build(2*i+1,mid+1,r);
	tree[i].sums=tree[2*i].sums+tree[2*i+1].sums;//回溯更新sum 
	tree[i].sumc=tree[2*i].sumc+tree[2*i+1].sumc;
}
void push_down(int i){
	if(tree[i].lz!=0){
		ll v=tree[i].lz;
		tree[2*i].lz+=v;
		tree[2*i+1].lz+=v;
		double ls1=tree[2*i].sums,lc1=tree[2*i].sumc,ls2=tree[2*i+1].sums,lc2=tree[2*i+1].sumc;		
		tree[2*i].sums=ls1*cos(v)+lc1*sin(v);
		tree[2*i].sumc=lc1*cos(v)-ls1*sin(v);
		tree[2*i+1].sums=ls2*cos(v)+lc2*sin(v);
		tree[2*i+1].sumc=lc2*cos(v)-ls2*sin(v);
		tree[i].lz=0;
	}
}
void add(int i,int l,int r,ll k){
	if(tree[i].l>=l&&tree[i].r<=r){
		double ls=tree[i].sums,lc=tree[i].sumc;
		tree[i].sums=ls*cos(k)+lc*sin(k);
		tree[i].sumc=lc*cos(k)-ls*sin(k);
		tree[i].lz+=k;
		return;
	}
	push_down(i);//本节点已更新完毕,把懒标记传递给子树 
	if(tree[2*i].r>=l) add(i*2,l,r,k);
	if(tree[2*i+1].l<=r) add(2*i+1,l,r,k);
	tree[i].sums=tree[2*i].sums+tree[2*i+1].sums;//回溯更新sum 
	tree[i].sumc=tree[2*i].sumc+tree[2*i+1].sumc; 
}
double search(int i,int l,int r){
	if(tree[i].l>=l&&tree[i].r<=r) return tree[i].sums;
	if(tree[i].l>r||tree[i].r<l) return 0;
	push_down(i);
	double ans=0;
	if(tree[2*i].r>=l) ans+=search(2*i,l,r);
	if(tree[2*i+1].l<=r) ans+=search(2*i+1,l,r);
	return ans;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++){
	scanf("%d",&a);
	s[i]=sin(a);
	c[i]=cos(a);
}
build(1,1,n);
scanf("%d",&m);
ll x,y,k;
while(m--){
	scanf("%lld%lld%lld",&a,&x,&y);
	if(a==1){
		scanf("%lld",&k);
		add(1,x,y,k);
	}
	else printf("%.1f\n",round(search(1,x,y)*10)/10);
}	
	return 0;
}

你可能感兴趣的:(刷题记录,算法,leetcode,数据结构)