更多 CSP 认证考试题目题解可以前往:CSP-CCF 认证考试真题题解
原题链接: 202309-3 梯度求解
时间限制: 1.0s
内存限制: 512.0MB
背景
西西艾弗岛运营公司近期在大力推广智能化市政管理系统。这套系统是由西西艾弗岛信息中心研发的。它的主要目的是,通过详细评估岛上各处的市政设施的状况,来指导市政设施的维护和更新。这套系统的核心是一套智能化的传感器网络,它能够自动地对岛上的市政设施进行评估。对市政设施的维护是需要一定成本的,而年久失修的市政设施也可能给岛上的居民造成损失。为了能够平衡成本和收益,信息中心研发了一款数学模型,描述这些变量和损益之间的复杂数学关系。要想得到最优化的成本,就要依靠梯度下降算法来求解。
梯度下降算法中,求解函数在一点处对某一自变量的偏导数是十分重要的。小 C 负责实现这个功能,但是具体的技术实现,他还是一头雾水,希望你来帮助他完成这个任务。
问题描述
设被求算的函数 u = f ( x 1 , x 2 , … , x n ) u=f(x_1, x_2, \dots, x_n) u=f(x1,x2,…,xn),本题目要求你求出 u u u 对 x i x_i xi 在 ( a 1 , a 2 , … , a n ) (a_1, a_2, \dots, a_n) (a1,a2,…,an) 处的偏导数 ∂ u ∂ x i ( a 1 , a 2 , … , a n ) \frac{\partial u}{\partial x_i}(a_1, a_2, \dots, a_n) ∂xi∂u(a1,a2,…,an)。
求算多元函数在一点处对某一自变量的偏导数的方法是:将函数的该自变量视为单一自变量,其余自变量认为是常数,运用一元函数求导的方法求出该偏导数表达式,再代入被求算的点的坐标即可。
例如,要求算 u = x 1 ⋅ x 1 ⋅ x 2 u=x_1 \cdot x_1 \cdot x_2 u=x1⋅x1⋅x2 对 x 1 x_1 x1 在 ( 1 , 2 ) (1, 2) (1,2) 处的偏导数,可以将 x 2 x_2 x2 视为常数,依次应用求导公式。先应用乘法的求导公式: ( x 1 ⋅ ( x 1 ⋅ x 2 ) ) ′ = x 1 ′ ( x 1 ⋅ x 2 ) + x 1 ( x 1 ⋅ x 2 ) ′ (x_1\cdot(x_1 \cdot x_2))' = x_1'(x_1\cdot x_2) +x_1(x_1\cdot x_2)' (x1⋅(x1⋅x2))′=x1′(x1⋅x2)+x1(x1⋅x2)′;再应用常数与变量相乘的求导公式,得到 x 1 ′ ⋅ x 1 ⋅ x 2 + x 1 ⋅ x 2 ⋅ x 1 ′ x_1' \cdot x_1 \cdot x_2 + x_1 \cdot x_2 \cdot x_1' x1′⋅x1⋅x2+x1⋅x2⋅x1′;最后应用公式 x ′ = 1 x' = 1 x′=1 得到 1 ⋅ x 1 ⋅ x 2 + x 1 ⋅ x 2 ⋅ 1 1 \cdot x_1 \cdot x_2 + x_1 \cdot x_2 \cdot 1 1⋅x1⋅x2+x1⋅x2⋅1。整理得 ∂ u ∂ x 1 = 2 x 2 ⋅ x 1 \frac{\partial u}{\partial x_1} = 2 x_2 \cdot x_1 ∂x1∂u=2x2⋅x1。再代入 ( 1 , 2 ) (1, 2) (1,2) 得到 ∂ u ∂ x 1 ( 1 , 2 ) = 4 \frac{\partial u}{\partial x_1}(1, 2) = 4 ∂x1∂u(1,2)=4。
常见的求导公式有:
- c ′ = 0 ( c 是常数) c' = 0 \ \text{(}c\text{是常数)} c′=0 (c是常数)
- x ′ = 1 x' = 1 x′=1
- ( u + v ) ′ = u ′ + v ′ (u + v)' = u' + v' (u+v)′=u′+v′
- ( c u ) ′ = c u ′ ( c 是常数) (cu)' = cu' \ \text{(}c\text{是常数)} (cu)′=cu′ (c是常数)
- ( u − v ) ′ = u ′ − v ′ (u - v)' = u' - v' (u−v)′=u′−v′
- ( u v ) ′ = u ′ v + u v ′ (uv)' = u'v + uv' (uv)′=u′v+uv′
本题目中,你需要求解的函数 f f f 仅由常数、自变量和它们的加法、减法、乘法组成。且为程序识读方便,函数表达式已经被整理为逆波兰式(后缀表达式)的形式。例如, x 1 ⋅ x 1 ⋅ x 2 x_1 \cdot x_1 \cdot x_2 x1⋅x1⋅x2 的逆波兰式为 x1 x1 * x2 *。逆波兰式即为表达式树的后序遍历的结果。若要从逆波兰式还原原始计算算式,可以按照这一方法进行:假设存在一个空栈 S S S,依次读取逆波兰式的每一个元素,若读取到的是变量或常量,则将其压入 S S S 中;若读取到的是计算符号,则从 S S S 中取出两个元素,进行相应运算,再将结果压入 S S S 中。最后,若 S S S 中存在唯一的元素,则该表达式合法,其值即为该元素的值。例如对于逆波兰式 x1 x1 * x2 *,按上述方法读取,栈 S S S 的变化情况依次为(左侧是栈底,右侧是栈顶):
- x 1 x_1 x1;
- x 1 x_1 x1, x 1 x_1 x1;
- ( x 1 ⋅ x 1 ) (x_1 \cdot x_1) (x1⋅x1);
- ( x 1 ⋅ x 1 ) (x_1 \cdot x_1) (x1⋅x1), x 2 x_2 x2;
- ( ( x 1 ⋅ x 1 ) ⋅ x 2 ) ((x_1 \cdot x_1) \cdot x_2) ((x1⋅x1)⋅x2)。
输入格式
从标准输入读入数据。
输入的第一行是由空格分隔的两个正整数 n n n、 m m m,分别表示要求解函数中所含自变量的个数和要求解的偏导数的个数。
输入的第二行是一个逆波兰式,表示要求解的函数 f f f。其中,每个元素用一个空格分隔,每个元素可能是:
- 一个自变量 x i x_i xi,用字符 x 后接一个正整数表示,表示第 i i i 个自变量,其中 i = 1 , 2 , … , n i = 1, 2, \dots, n i=1,2,…,n。例如,x1 表示第一个自变量 x 1 x_1 x1。
- 一个整常数,用十进制整数表示,其值在 − 1 0 5 -10^5 −105 到 1 0 5 10^5 105 之间。
- 一个运算符,用 + 表示加法,- 表示减法,* 表示乘法。
输入的第三行到第
m
+
2
m+2
m+2 行,每行有
n
+
1
n+1
n+1 个用空格分隔的整数。其中第一个整数是要求偏导数的自变量的编号
i
=
1
,
2
,
…
,
n
i = 1, 2, \dots, n
i=1,2,…,n,随后的整数是要求算的点的坐标
a
1
,
a
2
,
…
,
a
n
a_1, a_2, \dots, a_n
a1,a2,…,an。
输入数据保证,对于所有的
i
=
1
,
2
,
…
,
n
i = 1, 2, \dots, n
i=1,2,…,n,
a
i
a_i
ai 都在
−
1
0
5
-10^5
−105 到
1
0
5
10^5
105 之间。
输出格式
输出到标准输出中。
输出 m m m 行,每行一个整数,表示对应的偏导数对 1 0 9 + 7 10^9+7 109+7 取模的结果。即若结果为 y y y,输出为 k k k,则保证存在整数 t t t,满足 y = k + t ⋅ ( 1 0 9 + 7 ) y = k + t \cdot (10^9+7) y=k+t⋅(109+7) 且 0 ≤ k < 1 0 9 + 7 0 \le k < 10^9+7 0≤k<109+7。
样例 1 输入
2 2
x1 x1 x1 * x2 + *
1 2 3
2 3 4
样例 1 输出
15
3
样例 1 说明
读取逆波兰式,可得被求导的式子是: u = x 1 ⋅ ( x 1 ⋅ x 1 + x 2 ) u = x_1 \cdot (x_1 \cdot x_1 + x_2) u=x1⋅(x1⋅x1+x2),即 u = x 1 3 + x 1 x 2 u = x_1^3 + x_1x_2 u=x13+x1x2。
对 x 1 x_1 x1 求偏导得 ∂ u ∂ x 1 = 3 x 1 2 + x 2 \frac{\partial u}{\partial x_1} = 3x_1^2 + x_2 ∂x1∂u=3x12+x2。代入 ( 2 , 3 ) (2, 3) (2,3) 得到 ∂ u ∂ x 1 ( 2 , 3 ) = 15 \frac{\partial u}{\partial x_1}(2, 3) = 15 ∂x1∂u(2,3)=15。
对 x 2 x_2 x2 求偏导得 ∂ u ∂ x 2 = x 1 \frac{\partial u}{\partial x_2} = x_1 ∂x2∂u=x1。代入 ( 3 , 4 ) (3, 4) (3,4) 得到 ∂ u ∂ x 2 ( 3 , 4 ) = 3 \frac{\partial u}{\partial x_2}(3, 4) = 3 ∂x2∂u(3,4)=3。
样例 2 输入
3 5
x2 x2 * x2 * 0 + -100000 -100000 * x2 * -
3 100000 100000 100000
2 0 0 0
2 0 -1 0
2 0 1 0
2 0 100000 0
样例 2 输出
0
70
73
73
999999867
样例 2 说明
读取逆波兰式,可得被求导的式子是: u = x 2 ⋅ x 2 ⋅ x 2 + 0 − ( − 1 0 5 ) ⋅ ( − 1 0 5 ) ⋅ x 2 u = x_2 \cdot x_2 \cdot x_2 + 0 - (-10^{5}) \cdot (-10^{5}) \cdot x_2 u=x2⋅x2⋅x2+0−(−105)⋅(−105)⋅x2,即 u = x 2 3 − 1 0 10 x 2 u = x_2^3 - 10^{10}x_2 u=x23−1010x2。
因为 u u u 中实际上不含 x 1 x_1 x1 和 x 3 x_3 x3,对这两者求偏导结果均为 0 0 0。
对 x 2 x_2 x2 求偏导得 ∂ u ∂ x 2 = 3 x 2 2 − 1 0 10 \frac{\partial u}{\partial x_2} = 3x_2^2 - 10^{10} ∂x2∂u=3x22−1010。
评测用例规模与约定
测试点 | n n n | m m m | 表达式的性质 |
---|---|---|---|
1, 2 | = 1 =1 =1 | ≤ 100 \le 100 ≤100 | 仅含有 1 个元素 |
3, 4 | = 1 =1 =1 | ≤ 100 \le 100 ≤100 | 仅含有一个运算符 |
5, 6 | ≤ 10 \le 10 ≤10 | ≤ 100 \le 100 ≤100 | 含有不超过 120 个元素,且不含乘法 |
7, 8 | ≤ 10 \le 10 ≤10 | ≤ 100 \le 100 ≤100 | 含有不超过 120 个元素 |
9, 10 | ≤ 100 \le 100 ≤100 | ≤ 100 \le 100 ≤100 | 含有不超过 120 个元素 |
提示
C++ 中可以使用 std::getline(std::cin, str) 读入字符串直到行尾。
当计算整数 n n n 对 M M M 的模时,若 n n n 为负数,需要注意将结果调整至区间 [ 0 , M ) [0, M) [0,M) 内。
题解
对于仅含有数字的后缀表达式求值,比如后缀表达式 1 2 + 3 4 + *
,对应的中缀表达式为
(
1
+
2
)
∗
(
3
+
4
)
(1+2)*(3+4)
(1+2)∗(3+4),使用程序来计算的过程中仅需要用到一个栈即可。当读入的为数字时将数字压栈;当读入的为运算符号时,用次栈顶作为第一运算数,栈顶作为第二运算数进行运算,将两个数出栈后,将计算结果压回栈中即可,最后栈中剩余的数字即为后缀表达式的值。
对于题目中的表达式,如果对 x 1 x_1 x1 求导, x 2 , x 3 , ⋯ x_2,x_3,\cdots x2,x3,⋯ 可以当做已知的数带入,这样并不会影响求导结果。
对于每一个询问 (均以对
x
1
x_1
x1 求导为例),先将后缀表达式通过计算得到一个关于
x
1
x_1
x1 的多项式
f
(
x
1
)
=
a
0
+
a
1
x
1
+
a
2
x
1
2
+
a
3
x
1
3
+
⋯
f(x_1)=a_0+a_1x_1+a_2x_1^2+a_3x_1^3+\cdots
f(x1)=a0+a1x1+a2x12+a3x13+⋯。考虑用 vector<int>
来表示多项式,输入的数字看成
0
0
0 次项系数,输入对应需要求导的变量就将
1
1
1 次项系数记为
1
1
1,输入其他变量时直接带入对应的值与数字做类似的处理,然后重载 +
、-
、*
三个运算符,即可和普通的后缀表达式求值一致。
然后对其进行求导得到结果 f ′ ( x 1 ) = a 1 + 2 a 2 x 1 + 3 a 3 x 1 2 + ⋯ f'(x_1)=a_1+2a_2x_1+3a_3x_1^2+\cdots f′(x1)=a1+2a2x1+3a3x12+⋯,并带入 x 1 x_1 x1 的值计算结果即可。
注意运算过程中及时对计算结果进行取模操作,否则有可能因为溢出而引发答案错误。
时间复杂度: O ( n m ) \mathcal{O}(nm) O(nm)。
参考代码(15ms,2.960MB)
/*
Created by Pujx on 2024/2/5.
*/
#pragma GCC optimize(2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
//#define int long long
//#define double long double
using i64 = long long;
using ui64 = unsigned long long;
using i128 = __int128;
#define inf (int)0x3f3f3f3f3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define yn(x) cout << (x ? "yes" : "no") << endl
#define Yn(x) cout << (x ? "Yes" : "No") << endl
#define YN(x) cout << (x ? "YES" : "NO") << endl
#define mem(x, i) memset(x, i, sizeof(x))
#define cinarr(a, n) for (int i = 1; i <= n; i++) cin >> a[i]
#define cinstl(a) for (auto& x : a) cin >> x;
#define coutarr(a, n) for (int i = 1; i <= n; i++) cout << a[i] << " \n"[i == n]
#define coutstl(a) for (const auto& x : a) cout << x << ' '; cout << endl
#define all(x) (x).begin(), (x).end()
#define md(x) (((x) % mod + mod) % mod)
#define ls (s << 1)
#define rs (s << 1 | 1)
#define ft first
#define se second
#define pii pair<int, int>
#ifdef DEBUG
#include "debug.h"
#else
#define dbg(...) void(0)
#endif
const int N = 2e5 + 5;
//const int M = 1e5 + 5;
//const int mod = 998244353;
const int mod = 1e9 + 7;
//template <typename T> T ksm(T a, i64 b) { T ans = 1; for (; b; a = 1ll * a * a, b >>= 1) if (b & 1) ans = 1ll * ans * a; return ans; }
//template <typename T> T ksm(T a, i64 b, T m = mod) { T ans = 1; for (; b; a = 1ll * a * a % m, b >>= 1) if (b & 1) ans = 1ll * ans * a % m; return ans; }
int a[N];
int n, m, t, k, q;
using Poly = vector<int>;
namespace Polynomial {
Poly operator + (Poly a, Poly b) {
a.resize(max(a.size(), b.size()));
for (int i = 0; i < b.size(); i++) a[i] = a[i] + b[i] >= mod ? a[i] + b[i] - mod : a[i] + b[i];
while (a.size() > 1 && !a.back()) a.pop_back();
return a;
}
Poly operator - (Poly a, Poly b) {
a.resize(max(a.size(), b.size()));
for (int i = 0; i < b.size(); i++) a[i] = a[i] - b[i] < 0 ? a[i] - b[i] + mod : a[i] - b[i];
while (a.size() > 1 && !a.back()) a.pop_back();
return a;
}
Poly operator * (Poly a, Poly b) {
Poly c(a.size() + b.size() - 1);
for (int i = 0; i < a.size(); i++)
for (int j = 0; j < b.size(); j++)
c[i + j] = (c[i + j] + 1ll * a[i] * b[j]) % mod;
while (c.size() > 1 && !c.back()) c.pop_back();
return c;
}
}
using namespace Polynomial;
void work() {
cin >> n >> m;
string s;
getline(cin, s);
getline(cin, s);
while (m--) {
int tar; cin >> tar;
for (int i = 1; i <= n; i++) cin >> a[i], a[i] = (a[i] + mod) % mod;
stringstream in(s);
stack<Poly> st;
string tem;
while (in >> tem) {
if (tem == "+" || tem == "-" || tem == "*") {
Poly y = st.top(); st.pop();
Poly x = st.top(); st.pop();
if (tem == "+") st.push(x + y);
else if (tem == "-") st.push(x - y);
else st.push(x * y);
}
else if (tem[0] == 'x') {
if (stoi(tem.substr(1)) == tar) st.push(Poly{0, 1});
else st.push(Poly{a[stoi(tem.substr(1))]});
}
else st.push(Poly{(stoi(tem) + mod) % mod});
}
Poly x = st.top(); st.pop();
int ans = 0, pw = 1;
for (int i = 1; i < x.size(); i++)
ans = (ans + 1ll * x[i] * i % mod * pw % mod) % mod, pw = 1ll * pw * a[tar] % mod;
cout << ans << endl;
}
}
signed main() {
#ifdef LOCAL
freopen("C:\\Users\\admin\\CLionProjects\\Practice\\data.in", "r", stdin);
freopen("C:\\Users\\admin\\CLionProjects\\Practice\\data.out", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int Case = 1;
//cin >> Case;
while (Case--) work();
return 0;
}
/*
_____ _ _ _ __ __
| _ \ | | | | | | \ \ / /
| |_| | | | | | | | \ \/ /
| ___/ | | | | _ | | } {
| | | |_| | | |_| | / /\ \
|_| \_____/ \_____/ /_/ \_\
*/
关于代码的亿点点说明:
- 代码的主体部分位于
void work()
函数中,另外会有部分变量申明、结构体定义、函数定义在上方。#pragma ...
是用来开启 O2、O3 等优化加快代码速度。- 中间一大堆
#define ...
是我习惯上的一些宏定义,用来加快代码编写的速度。"debug.h"
头文件是我用于调试输出的代码,没有这个头文件也可以正常运行(前提是没定义DEBUG
宏),在程序中如果看到dbg(...)
是我中途调试的输出的语句,可能没删干净,但是没有提交上去没有任何影响。ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
这三句话是用于解除流同步,加快输入cin
输出cout
速度(这个输入输出流的速度很慢)。在小数据量无所谓,但是在比较大的读入时建议加这句话,避免读入输出超时。如果记不下来可以换用scanf
和printf
,但使用了这句话后,cin
和scanf
、cout
和printf
不能混用。- 将
main
函数和work
函数分开写纯属个人习惯,主要是为了多组数据。