“计算图”(computational graph)是现代深度学习系统的基础执行引擎,提供了一种表示任意数学表达式的方法,例如用有向无环图表示的神经网络。 图中的节点表示基本操作或输入变量,边表示节点之间的中间值的依赖性。 例如,下图就是一个函数f(x1,x2)=lnx1+x1x2−sinx2的计算图。
现在给定一个计算图,请你根据所有输入变量计算函数值及其偏导数(即梯度)。 例如,给定输入x1=2,x2=5,上述计算图获得函数值 f(2,5)=ln(2)+2×5−sin(5)=11.652;并且根据微分链式法则,上图得到的梯度 ∇f=[∂f/∂x1,∂f/∂x2]=[1/x1+x2,x1−cosx2]=[5.500,1.716]。
知道你已经把微积分忘了,所以这里只要求你处理几个简单的算子:加法、减法、乘法、指数(ex,即编程语言中的 exp(x) 函数)、对数(lnx,即编程语言中的 log(x) 函数)和正弦函数(sinx,即编程语言中的 sin(x) 函数)。
友情提醒:
- 常数的导数是 0;x 的导数是 1;ex 的导数还是 ex;lnx 的导数是 1/x;sinx 的导数是 cosx。
- 回顾一下什么是偏导数:在数学中,一个多变量的函数的偏导数,就是它关于其中一个变量的导数而保持其他变量恒定。在上面的例子中,当我们对 x1 求偏导数 ∂f/∂x1 时,就将 x2 当成常数,所以得到 lnx1 的导数是 1/x1,x1x2 的导数是 x2,sinx2 的导数是 0。
- 回顾一下链式法则:复合函数的导数是构成复合这有限个函数在相应点的导数的乘积,即若有 u=f(y),y=g(x),则 du/dx=du/dy⋅dy/dx。例如对 sin(lnx) 求导,就得到 cos(lnx)⋅(1/x)。
如果你注意观察,可以发现在计算图中,计算函数值是一个从左向右进行的计算,而计算偏导数则正好相反。
输入格式:
输入在第一行给出正整数 N(≤5×10^4),为计算图中的顶点数。
以下 N 行,第 i 行给出第 i 个顶点的信息,其中 i=0,1,⋯,N−1。第一个值是顶点的类型编号,分别为:
- 0 代表输入变量
- 1 代表加法,对应 x1+x2
- 2 代表减法,对应 x1−x2
- 3 代表乘法,对应 x1×x2
- 4 代表指数,对应 ex
- 5 代表对数,对应 lnx
- 6 代表正弦函数,对应 sinx
对于输入变量,后面会跟它的双精度浮点数值;对于单目算子,后面会跟它对应的单个变量的顶点编号(编号从 0 开始);对于双目算子,后面会跟它对应两个变量的顶点编号。
题目保证只有一个输出顶点(即没有出边的顶点,例如上图最右边的 -
),且计算过程不会超过双精度浮点数的计算精度范围。
输出格式:
首先在第一行输出给定计算图的函数值。在第二行顺序输出函数对于每个变量的偏导数的值,其间以一个空格分隔,行首尾不得有多余空格。偏导数的输出顺序与输入变量的出现顺序相同。输出小数点后 3 位。
输入样例:
7
0 2.0
0 5.0
5 0
3 0 1
6 1
1 2 3
2 5 4
输出样例:
11.652
5.500 1.716
题意: 正如题目中所说的,给出一个有向无环图,同时给出若干输入变量,求最终函数值以及各变量的偏导数。
分析: 对于有向无环图很容易想到拓扑排序,对于每个结点都需要开四个数组,一个记录结点操作符,一个记录它上面的数值,还有两个分别记录操作数编号。一个结点的入边只有0、1、2这三种情况,0的时候就是起始的输入变量,1的时候就是单目运算符,2的时候就是双目运算符,对于后面两种结点,它们的数值需要先得到其前驱点的数值才能计算,而拓扑序正是满足了这一点,当某结点加入队列时其前驱上的各种信息都已经获得了,所以可以直接拿来用,比如加法:num[now] = num[a[now]]+num[b[now]],其中a[now]和b[now]分别是now点的两个操作数编号,这其实是有点动态规划的思想在的。
这样就求出了最终输出的函数值,接下来考虑每个输入变量的偏导数。其实求偏导数思想和上面一样,也是一个动态规划的思想,可以开一个数组p[i][j]表示对于第j个变量遍历到i点时其偏导数值,由于始终是以拓扑序遍历结点,所以到当前结点时其前驱结点的p值都已经获得了,还是以加法为例:p[now][i] = p[a[now]][i]+p[b[now]][i],这样就求得各变量的偏导数了。
最终只需要跑一遍拓扑排序,时间复杂度为O(n)。
下面分析一下测试点:
测试点编号 | 数据描述 |
0 | 大样例,存在2个输入变量 |
1 | 存在2个输入变量 |
2 | 只有1个输入变量 |
3 | 只有1个输入变量 |
4 | 只有1个输入变量 |
5 | 存在200个输入变量 |
具体代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include <queue>
using namespace std;
int op[50005];//操作符
int a[50005], b[50005];//操作数(如果有的话)
int in[50005];//入度
double num[50005];//结点上的值
vector<double> p[50005];//直接二维数组开不下
vector<int> to[50005];//邻接表建图
vector<int> x;//保存输入变量
signed main(){
int n;
cin >> n;
for(int i = 0; i < n; i++){
scanf("%d", &op[i]);
if(op[i] == 0){
scanf("%lf", &num[i]);
x.push_back(i);
}
if(op[i] == 1)
scanf("%d%d", &a[i], &b[i]), to[a[i]].push_back(i), to[b[i]].push_back(i), in[i] += 2;
if(op[i] == 2)
scanf("%d%d", &a[i], &b[i]), to[a[i]].push_back(i), to[b[i]].push_back(i), in[i] += 2;
if(op[i] == 3)
scanf("%d%d", &a[i], &b[i]), to[a[i]].push_back(i), to[b[i]].push_back(i), in[i] += 2;
if(op[i] == 4)
scanf("%d", &a[i]), to[a[i]].push_back(i), in[i]++;
if(op[i] == 5)
scanf("%d", &a[i]), to[a[i]].push_back(i), in[i]++;
if(op[i] == 6)
scanf("%d", &a[i]), to[a[i]].push_back(i), in[i]++;
}
for(int i = 0; i < n; i++)
for(int j = 0; j < x.size(); j++)
p[i].push_back(0);
queue<int> q;
for(int i = 0; i < n; i++)
if(in[i] == 0)
q.push(i);
for(int i = 0; i < x.size(); i++)
p[x[i]][i] = 1;
int final;//记录终点
while(q.size()){
int now = q.front();
q.pop();
if(op[now] == 1){
num[now] = num[a[now]]+num[b[now]];
for(int i = 0; i < x.size(); i++) p[now][i] = p[a[now]][i]+p[b[now]][i];
}
if(op[now] == 2){
num[now] = num[a[now]]-num[b[now]];
for(int i = 0; i < x.size(); i++) p[now][i] = p[a[now]][i]-p[b[now]][i];
}
if(op[now] == 3){
num[now] = num[a[now]]*num[b[now]];
for(int i = 0; i < x.size(); i++) p[now][i] = p[a[now]][i]*num[b[now]]+p[b[now]][i]*num[a[now]];
}
if(op[now] == 4){
num[now] = exp(num[a[now]]);
for(int i = 0; i < x.size(); i++) p[now][i] = num[now]*p[a[now]][i];
}
if(op[now] == 5){
num[now] = log(num[a[now]]);
for(int i = 0; i < x.size(); i++) p[now][i] = 1/num[a[now]]*p[a[now]][i];
}
if(op[now] == 6){
num[now] = sin(num[a[now]]);
for(int i = 0; i < x.size(); i++) p[now][i] = cos(num[a[now]])*p[a[now]][i];
}
for(int i = 0; i < to[now].size(); i++){
int v = to[now][i];
in[v]--;
if(in[v] == 0) q.push(v);
}
final = now;
}
printf("%.3f\n", num[final]);
printf("%.3f", p[final][0]);
for(int i = 1; i < x.size(); i++)
printf(" %.3f", p[final][i]);
return 0;
}