CCF-CSP认证考试 202309-3 梯度求解 100分题解

更多 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) xiu(a1,a2,,an)

求算多元函数在一点处对某一自变量的偏导数的方法是:将函数的该自变量视为单一自变量,其余自变量认为是常数,运用一元函数求导的方法求出该偏导数表达式,再代入被求算的点的坐标即可。

例如,要求算 u = x 1 ⋅ x 1 ⋅ x 2 u=x_1 \cdot x_1 \cdot x_2 u=x1x1x2 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(x1x2))=x1(x1x2)+x1(x1x2);再应用常数与变量相乘的求导公式,得到 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' x1x1x2+x1x2x1;最后应用公式 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 1x1x2+x1x21。整理得 ∂ u ∂ x 1 = 2 x 2 ⋅ x 1 \frac{\partial u}{\partial x_1} = 2 x_2 \cdot x_1 x1u=2x2x1。再代入 ( 1 , 2 ) (1, 2) (1,2) 得到 ∂ u ∂ x 1 ( 1 , 2 ) = 4 \frac{\partial u}{\partial x_1}(1, 2) = 4 x1u(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' (uv)=uv
  • ( u v ) ′ = u ′ v + u v ′ (uv)' = u'v + uv' (uv)=uv+uv

本题目中,你需要求解的函数 f f f 仅由常数、自变量和它们的加法、减法、乘法组成。且为程序识读方便,函数表达式已经被整理为逆波兰式(后缀表达式)的形式。例如, x 1 ⋅ x 1 ⋅ x 2 x_1 \cdot x_1 \cdot x_2 x1x1x2 的逆波兰式为 x1 x1 * x2 *。逆波兰式即为表达式树的后序遍历的结果。若要从逆波兰式还原原始计算算式,可以按照这一方法进行:假设存在一个空栈 S S S,依次读取逆波兰式的每一个元素,若读取到的是变量或常量,则将其压入 S S S 中;若读取到的是计算符号,则从 S S S 中取出两个元素,进行相应运算,再将结果压入 S S S 中。最后,若 S S S 中存在唯一的元素,则该表达式合法,其值即为该元素的值。例如对于逆波兰式 x1 x1 * x2 *,按上述方法读取,栈 S S S 的变化情况依次为(左侧是栈底,右侧是栈顶):

  1. x 1 x_1 x1
  2. x 1 x_1 x1 x 1 x_1 x1
  3. ( x 1 ⋅ x 1 ) (x_1 \cdot x_1) (x1x1)
  4. ( x 1 ⋅ x 1 ) (x_1 \cdot x_1) (x1x1) x 2 x_2 x2
  5. ( ( x 1 ⋅ x 1 ) ⋅ x 2 ) ((x_1 \cdot x_1) \cdot x_2) ((x1x1)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 0k<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(x1x1+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 x1u=3x12+x2。代入 ( 2 , 3 ) (2, 3) (2,3) 得到 ∂ u ∂ x 1 ( 2 , 3 ) = 15 \frac{\partial u}{\partial x_1}(2, 3) = 15 x1u(2,3)=15

x 2 x_2 x2 求偏导得 ∂ u ∂ x 2 = x 1 \frac{\partial u}{\partial x_2} = x_1 x2u=x1。代入 ( 3 , 4 ) (3, 4) (3,4) 得到 ∂ u ∂ x 2 ( 3 , 4 ) = 3 \frac{\partial u}{\partial x_2}(3, 4) = 3 x2u(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=x2x2x2+0(105)(105)x2,即 u = x 2 3 − 1 0 10 x 2 u = x_2^3 - 10^{10}x_2 u=x231010x2

因为 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} x2u=3x221010

评测用例规模与约定

测试点 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;
}
/*
     _____   _   _       _  __    __
    |  _  \ | | | |     | | \ \  / /
    | |_| | | | | |     | |  \ \/ /
    |  ___/ | | | |  _  | |   }  {
    | |     | |_| | | |_| |  / /\ \
    |_|     \_____/ \_____/ /_/  \_\
*/

关于代码的亿点点说明:

  1. 代码的主体部分位于 void work() 函数中,另外会有部分变量申明、结构体定义、函数定义在上方。
  2. #pragma ... 是用来开启 O2、O3 等优化加快代码速度。
  3. 中间一大堆 #define ... 是我习惯上的一些宏定义,用来加快代码编写的速度。
  4. "debug.h" 头文件是我用于调试输出的代码,没有这个头文件也可以正常运行(前提是没定义 DEBUG 宏),在程序中如果看到 dbg(...) 是我中途调试的输出的语句,可能没删干净,但是没有提交上去没有任何影响。
  5. ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); 这三句话是用于解除流同步,加快输入 cin 输出 cout 速度(这个输入输出流的速度很慢)。在小数据量无所谓,但是在比较大的读入时建议加这句话,避免读入输出超时。如果记不下来可以换用 scanfprintf,但使用了这句话后,cinscanfcoutprintf 不能混用。
  6. main 函数和 work 函数分开写纯属个人习惯,主要是为了多组数据。
  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值