CCF-CSP 梯度求解
前言
这道题是一道模拟题,模拟计算导数。用到了栈,以及运算符二叉树的应用,总体来说比较麻烦。看了好多题解都是用次方的方式解决的,我根据官方的python部分题解写了c++版本,主要应用了递归以及二叉树的处理。
题解
混分
- 看到最后的测试点,1到2只有一个变量,甚至连运算符都没有,3到4只有一个运算符,n同样等于1。那么我们就可以直接根据测试点来解答,只求一个运算符以及一个变量的情况(40分)
解题思路
构建二叉树
- 首先我们要读入字符串,随后处理字符串。
- 然后是处理波兰表达式的方法:由题目所说,我们当碰到运算符的时候,出栈两个元素进行运算,并且将计算结果压入栈之中。
- 由于我们最后的目的是要构造出来相应的运算二叉树(叶子节点为变量或者是常数,非叶子节点是操作符)
- 所以我们可以转变为对节点的操作:当遇到变量或者常数的时候,创建节点,并且将节点压入栈之中。当遇到操作数的时候,先创建节点(当前的操作数),出栈两个节点,将三个节点组成二叉树的形式,再将根节点也就是操作符节点压入栈之中即可,重复以上操作,直到处理完成。得到我们的二叉树
处理二叉树(计算导数)
- 二叉树如图所示
表示的是X1 * X2
- 最简单的情况之下: 若是常数(除去求偏导的变量外的数),导数为0 , 所需要求的偏导变量为1;
- 处理二叉树,根据导数公式,我们根据不同的操作符采用不同的公式
- 若为+ 即 前导数加后导数 d1 + d2
- “-” :前导数减去后导数 d1 - d2
- “*” :d1 * v2 + d2 * v1;
- 所以完成每次导数,我们需要计算得出前后表达式的偏导结果,以及前后表达式的计算结果
- 递归结果我们可以设置成一个二维变量,第一个数代表该表达式的偏导结果,第二个数代表该表达式的计算结果。
其他注意
- 模运算 % 当为负数时,其结果还为负数,所以需要我们进行判断处理,将其转为正数
- int类型为最大为2的32次,但是我们计算过程之中可能出现超过这个数的情况,所以我们使用 long long 类型来进行计算
参考代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<bits/stdc++.h>
using namespace std;
//标记三种符号
#define CONST 1
#define X_n 2
#define OPE 3
typedef long long ll;
ll MO = 1e9 + 7;
//二叉树节点
struct oper_Node {
string val;
//表示类型
int type;
oper_Node* left = NULL;
oper_Node* right = NULL;
//构造函数
oper_Node(string x)
{
val = x;
}
};
// 计算导数,返回一个数组
vector<ll> cal(oper_Node* root);
// 将CONST 或者 X 转变为数字
ll trans_x(string a);
// 模运算
ll mod(ll x);
// 存储着变量的数值
vector<ll> val(122);
// 判断是否为偏导的变量
vector<bool> if_d(122,false);
int main()
{
int n, m;
scanf("%d %d", &n, &m);
string s;
//scanf使用之后会再缓冲区留下一个'\n' 需要我们处理
getchar();
//getline 读入字符串
getline(cin,s);
stack<oper_Node*> p;
int i = 0;
// 用栈来处理表达式
while (true)
{
string temp;
//注意边界情况,不要越界
while (s[i]!= '\0' && s[i] != ' ')
{
temp += s[i++];
}
//不是操作数
if(temp!="*" && temp!= "+" && temp!="-")
{
oper_Node* newnode = new oper_Node(temp);
if (temp[0] == 'x')
{
newnode->type = X_n;
}
else
{
newnode->type = CONST;
}
p.push(newnode);
}
//操作数
else
{
oper_Node* a = p.top();
p.pop();
oper_Node* b = p.top();
p.pop();
oper_Node* newnode = new oper_Node(temp);
newnode->type = OPE;
newnode->left = b;
newnode->right = a;
p.push(newnode);
}
if (s[i] == '\0')
{
break;
}
i++;
}
//最后剩余就是二叉树根节点
oper_Node* exp = p.top();
for (int i = 0; i < m; i++)
{
int x;
scanf("%d", &x);
char a = x + '0';
string target = "x";
target += a;
//if_d 判断是否需要求导
if_d[x] = true;
for (int j = 1; j <= n; j++)
{
scanf("%lld", &val[j]);
}
vector<ll> end = cal(exp);
printf("%lld\n",mod(end[0]));
//恢复标记
if_d[x] = false;
}
return 0;
}
vector<ll> cal(oper_Node* root)
{
// 常数的导数为0
if (root->type == CONST)
{
vector<ll> temp(2);
temp[0] = 0;
temp[1] = trans_x(root->val);
return temp;
}
//若为x看是否为变量,变量求导为1,常量为0
if (root->type == X_n)
{
string a = root->val;
int i = 1;
int x = 0; // X_n]
vector<ll> temp(2);
while (a[i] != '\0')
{
x = x * 10 + a[i] - '0';
i++;
}
if (if_d[x])
{
temp[0] = 1;
temp[1] = val[x]%MO;
}
else
{
temp[0] = 0;
temp[1] = val[x]%MO;
}
return temp;
}
//若为操作数 需要计算两侧的导数与两侧的计算结果
vector<ll> a = cal(root->left);
vector<ll> b = cal(root->right);
vector<ll> result(2);
if (root->val == "+")
{
result[0] = (a[0] + b[0])%MO;
result[1] = (a[1] + b[1])%MO;
return result;
}
else
if (root->val == "-")
{
result[0] = (a[0] - b[0])%MO;
result[1] = (a[1] - b[1])%MO;
return result;
}
else
{
result[0] = ((b[1] * a[0])%MO + (a[1] * b[0])%MO)%MO;
result[1] = (a[1] * b[1])%MO;
return result;
}
}
//将非操作数转为相应的数字
ll trans_x(string a)
{
if (a[0] == 'x')
{
int i = 1;
int x = 0; // X_n
while (a[i] != '\0')
{
x += x * 10 + a[i] - '0';
i++;
}
return val[x];
}
else
{
ll x=0;
int i = 0;
if (a[i] != '-') {
while (a[i] != '\0')
{
x = x * 10 + a[i] - '0';
i++;
}
}
else
{
x = 0 - (a[++i] - '0');
i++;
while (a[i] != '\0')
{
x = x * 10 - a[i] + '0';
i++;
}
}
return x;
}
}
ll mod(ll x)
{
if (x < 0) x = x + MO;
return x;
}