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

题目内容

小明给出了一个关于未知量 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;
}
相关推荐
kyriewen34 分钟前
用魔法打败魔法:我让AI替我去面试前端岗,AI面试官给我打了92分,还发了offer
前端·javascript·面试
Lkstar1 小时前
Vue keep-alive 原理全解:LRU 缓存策略、源码级理解
前端·vue.js·面试
全糖可乐气泡水2 小时前
Codex适配国产信创环境安装部署与技术适配全解析
开发语言·git·python·算法·百度
h_a_o777oah2 小时前
状态机+划分型 DP :深度解析K-划分问题下 DP 状态的转移逻辑(洛谷P2679 P2331 附C++代码)
c++·算法·动态规划·acm·状态机dp·划分型dp·滚动数组优化
plainGeekDev2 小时前
Android内存面试题:OOM都解决不了,性能优化从何谈起?
android·面试·kotlin
05候补工程师2 小时前
从算法理想向工程现实的跨越:SLAM 核心架构、思维误区与 Nav2 实战避坑指南
人工智能·算法·安全·架构·机器人
蝎子莱莱爱打怪3 小时前
👍🏻👍🏻6年381颗芯片+韬定律,华为重新定义半导体,为什么还有人喷???
后端·面试·程序员
手写码匠3 小时前
Android 17 适配实战指南:新特性解读、隐私变更与迁移全攻略
人工智能·深度学习·算法·aigc
珊瑚里的鱼4 小时前
leetcode42雨水
算法·leetcode
水木流年追梦4 小时前
大模型入门-大模型的推理策略
开发语言·python·算法·正则表达式·prompt