【算法导论】一道涉及到溢出处理的笔试题

题目内容

小明给出了一个关于未知量 x 的多项式。这个多项式以字符串的形式表示,它由若干个形如"(x-d)"或"(x+d)"的括号表达式相乘构成。其中,d 是一个 1 到 9 之间的数字字符。

小明想知道,当这个多项式完全展开后,x 的一次项(即 x^1 项)的系数是多少? 请计算这个系数,由于答案可能很大,请将答案对10007取模后输出。

输入描述

在一行上输入一个长度为5≤len<10^5的字符串 s,表示给定的多项式。

题目保证字符串 s 严格由若干个"(x-d)"或"(x+d)"形式的子串拼接而成,其中 d 是 1 到 9 的数字字符。因此,字符串的长度一定是 5 的倍数。

输出描述

输出一个整数,表示多项式展开后 x 的一次项系数。由于答案可能很大,请将答案对10007取模后输出。

示例 1

输入

scss 复制代码
(x-1)(x+5)

输出

复制代码
4

说明 在这个样例中,多项式为 (x-1)(x+5)。 展开后得到 x^2 + 5x - x - 5 = x^2 + 4x - 5。 其中 x 的一次项系数是 4。对 10007 取模后结果仍为 4。

示例 2

输入

scss 复制代码
(x-1)(x+2)(x+3)

输出

复制代码
1

在这个样例中,多项式为 (x-1)(x+2)(x+3)= x^3 + 4x^2 + x - 6。 其中 x 的一次项系数是 1。对 10007 取模后结果仍为 1。

题目分析

经过简单的数学推导,不难得出以下结论。

给定一个多项式 P(x) = (x + a1)(x + a2)...(x + an),其中 ai 是从输入的字符串中解析出的常数(即 +d 或 -d)。

当这个多项式展开后,会得到 n 个 x 的一次项。它们的系数之和,等于从这 n 个 ai 中任选 n-1 个进行相乘,然后将所有这些乘积相加的结果。

一种简单的思路如下:我们可以先计算出所有ai的累乘结果m,再将m分别除以各个ai(相当于得到除了当前ai之外的n-1个数的乘积),然后将它们相加即可。

C++ 复制代码
#include <iostream>
#include <string>
#include <vector>

int main() {
  std::string expr;
  std::cin >> expr;

  int64_t m = 1;
  std::vector<int> nums;
  for (auto i = 0; i < expr.size(); i += 5) {
    int num = expr[i + 3] - '0';
    if (expr[i + 2] == '-') {
      num = -num;
    }

    m *= num;
    nums.push_back(num);
  }

  if (nums.size() == 1) {
    std::cout << 1;
    return 0;
  }

  int64_t ans = 0;
  for (int i = 0; i < nums.size(); ++i) {
    ans += m / nums[i];
  }

  std::cout << ans % 10007;
}

但这么做有一个严重的问题。题目中s的长度可以达到10^5,也就是说输入的表达式中最多有10^5/5=20000个(x±ai)子串。即使这些ai的取值都为2,它们的乘积结果也高达2^20000。这是一个天文数字,远远超过了int64_t能够表达的范围。

不过幸运的是,取模运算对加法、减法、乘法满足分配律:

  1. (a + b) % p == ((a % p) + (b % p)) % p
  2. (a - b) % p == ((a % p) - (b % p)) % p
  3. (a * b) % p == ((a % p) * (b % p)) % p

因此我们设计的算法,应该在计算加法和乘法的过程中,对中间结果立即进行取模,才能保证运算过程中始终不发生溢出。同时避免直接使用除法。

解答代码

暴力解法

C++ 复制代码
#include <iostream>
#include <vector>

constexpr int mod_base = 10007;

int main() {
	std::string expr;
	std::cin >> expr;

	std::vector<int> nums;
	for (int i = 0; i < expr.size() / 5; ++i) {
		int num = expr[i * 5 + 3] - '0';
		if (expr[i * 5 + 2] == '-') {
			num = -num;
		}
		nums.push_back(num);
	}

	// 外层循环控制当前项中不选择nums中的哪个数
	int ans = 0;
	for (int i = 0; i < nums.size(); ++i) {
		// 内层循环用于累乘nums中的其他数,得到当前项
		int curr = 1;
		for (int j = 0; j < nums.size(); ++j) {
			if (j == i) continue;
			curr = (curr * nums[j]) % mod_base;
		}
		ans = (ans + curr) % mod_base;
	}

	std::cout << ans % mod_base << std::endl;
}

优化解法

我们可以使用前缀积和后缀积,用空间换时间。

C++ 复制代码
#include <iostream>
#include <vector>

constexpr int mod_base = 10007;

int main() {
	std::string expr;
	std::cin >> expr;

	std::vector<int> nums;
	for (int i = 0; i < expr.size() / 5; ++i) {
		int num = expr[i * 5 + 3] - '0';
		if (expr[i * 5 + 2] == '-') {
			num = -num;
		}
		nums.push_back(num);
	}

	// 计算前缀积
	std::vector<int> prefix(nums.size());
	prefix[0] = 1;
	for (int i = 1; i < nums.size(); ++i) {
		prefix[i] = (prefix[i - 1] * nums[i - 1]) % mod_base;
	}

	// 计算后缀积
	std::vector<int> suffix(nums.size());
	suffix[nums.size() - 1] = 1;
	for (int i = nums.size() - 2; 0 <= i; --i) {
		suffix[i] = (suffix[i + 1] * nums[i + 1]) % mod_base;
	}

	// 计算答案
	int ans = 0;
	for (int i = 0; i < nums.size(); ++i) {
		ans += (prefix[i] * suffix[i]) % mod_base;
	}

	std::cout << ans % mod_base << std::endl;
}
相关推荐
遥感知识服务几秒前
从局部阈值、双极化到暗地表剔除:NASA OPERA DSWx-S1全球动态水体算法拆解
大数据·人工智能·深度学习·神经网络·算法·机器学习
晚风醉蝶7 分钟前
1-13-TimSort
算法
Rain的Java大神之路12 分钟前
短信接口被狂刷怎么处理
java·运维·后端·web安全·面试·架构·xss
ZJU_统一阿萨姆16 分钟前
【算子开发】算子融合与Softmax_LayerNorm实现
人工智能·算法·语言模型·硬件架构
叩码以求索2 小时前
浅谈:前序遍历反转法求解N叉树的后序遍历
算法
官乐3 小时前
AI面试指南(多agent开发流程)
人工智能·面试·职场和发展
北风toto3 小时前
中缀、前缀、后缀表达式
算法·软件设计师
听取WA声一片(无恶意)3 小时前
CSP-J/CSP-S 深度优先搜索(DFS)完全讲义
c++·算法·深度优先
码匠许师傅4 小时前
【C++ 面试真题】27. 聊聊 C++ 的内存泄漏与内存布局
java·c++·面试
跟我一起学测试呀4 小时前
软件测试面试总结,备战金九银十
面试·职场和发展