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

题目内容

小明给出了一个关于未知量 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 + a[1])(x + a[2])...(x + a[n]),其中 a[i] 是从输入的字符串中解析出的常数(即 +d 或 -d)。

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

一种简单的思路如下:我们可以先计算出所有a[i]的累乘结果m,再将m分别除以各个a[i](相当于得到除了当前a[i]之外的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±a[i])子串。即使这些a[i]的取值都为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;
}
相关推荐
无敌最俊朗@2 小时前
Qt 自定义控件(继承 QWidget)面试核心指南
开发语言·qt·面试
清***鞋2 小时前
转行AI产品如何准备面试
人工智能·面试·职场和发展
成成成成成成果2 小时前
软件测试面试八股文(一)
面试·职场和发展·测试用例·压力测试
哈泽尔都3 小时前
运动控制教学——5分钟学会样条曲线算法!(三次样条曲线,B样条曲线)
c++·人工智能·算法·机器学习·matlab·贪心算法·机器人
小镇学者3 小时前
【NOI】在信奥赛中 什么是函数交互题?
算法
未知陨落3 小时前
LeetCode:62.N皇后
算法·leetcode
myw0712054 小时前
Leetcode94.二叉数的中序遍历练习
c语言·数据结构·笔记·算法
songx_994 小时前
leetcode(填充每个节点的下一个右侧节点指针 II)
java·数据结构·算法·leetcode
chenyuhao20244 小时前
vector深度求索(上)实用篇
开发语言·数据结构·c++·后端·算法·类和对象