0、一些假设

假设创建了一个DAI/USDT的UniswapPair交易对,假设目前的DAI和USDT的价格是1:1,DAI的地址排序小于USDT的地址排序,其中有一个用户lpUser负责存入和提取交易对,另一个用户swapUser负责交易。接下来会模拟4次操作,分别是:

a)、lpUser存入2000/2000的DAI/USDT,获取流动性

b)、lpUser再次存入2000/2000的DAI/USDT,获取流动性

c)、swapUser执行一次交易:使用100DAI换取USDT

d)、lpUser提取一半流动性

1、初始状态

UniswapV2Pair的状态:

1、reverse0:这个是DAI的数量,初始为0

2、reverse1:这个是USDT的数量,初始为0

3、KLast:reverse0*reverse1,初始为0

4、totalSupply:UniswapV2Pair本身是ERC20标准token,代币的名称是UNI-V2(其实就是流动性),定义在UniswapV2ERC20中,totalSupply是总的供应量,初始为0

5、balanceOf:不同地址持有的UNI-V2(流动性)数量,是个mapping类型数据结构,key是地址,value是流动性数量。其中feeTo是手续费地址,address(this)是指UniswapV2Pair本身,lpUser是指流动性提供者账户地址。

DAI的状态:

1、balanceOf:不同地址持有DAI的数量,是个mapping类型数据结构,key是地址,value是DAI数量。其中保存了UniswapV2Pair的DAI数量目前为0、lpUser的DAI数量目前为10000、swapUser的DAI数量目前为10000。

USDT的状态:

1、balanceOf:不同地址持有USDT的数量,是个mapping类型数据结构,key是地址,value是USDT数量。其中保存了UniswapV2Pair的USDT数量目前为0、lpUser的USDT数量目前为10000、swapUser的DAI数量目前为0。

2、lpUser存入2000/2000的DAI/USDT,获取流动性

第一次添加流动性,走的是UniswapV2Router02的addLiquidity方法:

function addLiquidity(address tokenA,address tokenB,uint amountADesired,uint amountBDesired,uint amountAMin,uint amountBMin,address to,uint deadline) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);liquidity = IUniswapV2Pair(pair).mint(to);}

可以看到代码先调用

TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);

这里的tokenA是DAI,因此把msg.sender(也就是lpUser)的2000个DAI转给了UniswapV2Pair

然后调用

TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);

这里的tokenA是USDT,因此把msg.sender(也就是lpUser)的2000个USDT转给了UniswapV2Pair

最后调用

liquidity = IUniswapV2Pair(pair).mint(to);

将计算流动性,然后分配给to地址,这个地址其实是lpUser的地址

接下来我们进入UniswapV2Pair的mint方法:

function mint(address to) external lock returns (uint liquidity) {(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savingsuint balance0 = IERC20(token0).balanceOf(address(this));uint balance1 = IERC20(token1).balanceOf(address(this));uint amount0 = balance0.sub(_reserve0);uint amount1 = balance1.sub(_reserve1);bool feeOn = _mintFee(_reserve0, _reserve1);uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFeeif (_totalSupply == 0) {liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens} else {liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);}require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');_mint(to, liquidity);_update(balance0, balance1, _reserve0, _reserve1);if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-dateemit Mint(msg.sender, amount0, amount1);}

这里先拿到了UniswapV2Pair持有的DAI的数量,也就是前面转入的数量2000个,因此balance0=2000

然后拿到了UniswapV2Pair持有的USDT的数量,也就是前面转入的数量2000个,因此balance1=2000

第一次存入的时候reverse0和reverse1都是0,因此amount0和amount1都是2000。

调用_mintFee计算手续费:

function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {address feeTo = IUniswapV2Factory(factory).feeTo();feeOn = feeTo != address(0);uint _kLast = kLast; // gas savingsif (feeOn) {if (_kLast != 0) {uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));uint rootKLast = Math.sqrt(_kLast);if (rootK > rootKLast) {uint numerator = totalSupply.mul(rootK.sub(rootKLast));uint denominator = rootK.mul(5).add(rootKLast);uint liquidity = numerator / denominator;if (liquidity > 0) _mint(feeTo, liquidity);}}} else if (_kLast != 0) {kLast = 0;}}

因为第一次kLast是0,所以不会计算手续费

然后计算流动性:

if (_totalSupply == 0) {liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens} else {liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);}require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');_mint(to, liquidity);_update(balance0, balance1, _reserve0, _reserve1);if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-dateemit Mint(msg.sender, amount0, amount1);

由于一开始totalSupply就是0,因此,计算的流动性是2000*2000的开根号然后减去最小流动性(由于USDT和DAI其实是带单位的,2000USDT在合约中其实是2000*10的18次方,而最小流动性定义为1000,和2000*10的18次方相比非常小),这里近似看成2000

然后把2000流动性分配给lpUser,然后调用_update更新reserve0为2000,更新reserve1为2000,然后更新kLast为2000*2000 = 4000000

至此第一次添加流动性完成。

3、lpUser再次存入2000/2000的DAI/USDT,获取流动性

过程和上一步差不多:

先把2000DAI从lpUser转到UniswapV2Pair

再把2000USDT从lpUser转到UniswapV2Pair

然后调用UniswapV2Pair的mint方法增发流动性,第一步是算手续费:

function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {address feeTo = IUniswapV2Factory(factory).feeTo();feeOn = feeTo != address(0);uint _kLast = kLast; // gas savingsif (feeOn) {if (_kLast != 0) {uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));uint rootKLast = Math.sqrt(_kLast);if (rootK > rootKLast) {uint numerator = totalSupply.mul(rootK.sub(rootKLast));uint denominator = rootK.mul(5).add(rootKLast);uint liquidity = numerator / denominator;if (liquidity > 0) _mint(feeTo, liquidity);}}} else if (_kLast != 0) {kLast = 0;}}

这里kLast是4000000不是0,所以会先计算rootK,也就是Math.sqrt(2000*2000)= 2000,而rootKLast也是2000,所以rootK>rootKLast的条件通不过,手续费还是不需要计算。

接下来需要计算实际增发的流动性:

if (_totalSupply == 0) {liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens} else {liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);}

走的是else分支,amount0是2000,_totalSupply是2000,_reserve0是2000,amount1是2000,_reserve1也是2000,所以实际算出来的流动性是2000

最后把2000流动性增发给用户,然后更新reserve0,reserve1和kLast的值。

4、swapUser执行一次交易,使用100DAI换取USDT

swap的操作使用的是UniswapV2Router02的swapExactTokensForTokens

function swapExactTokensForTokens(uint amountIn,uint amountOutMin,address[] calldata path,address to,uint deadline) external virtual override ensure(deadline) returns (uint[] memory amounts) {amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]);_swap(amounts, path, to);}

amountIn是100,代表swapUser付出的100DAI,amountOutMin是一个预计的值,防止输出过小导致明显的损失,path是一个交易路径,由于Uniswap可以通过中间币对进行链式交易,也就是说可以从DAI-->ETH-->USDT这样换USDT,所以它是个数组,address[i]表示交易链中第i个币的token地址,我们这里是DAI-->USDT,所以path只有两个值,to就是swapUser在USDT里的账户地址,deadline表示超时时间,如果交易执行时间超过了这个值,会进行回滚。

这里一上来先通过UniswapV2Library算出来当前执行交易获得的USDT数量(amounts的值),代码如下:

function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');amounts = new uint[](path.length);amounts[0] = amountIn;for (uint i; i < path.length - 1; i++) {(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);}}function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');uint amountInWithFee = amountIn.mul(997);uint numerator = amountInWithFee.mul(reserveOut);uint denominator = reserveIn.mul(1000).add(amountInWithFee);amountOut = numerator / denominator;}

上面一个函数就是执行链式交易,这里我们关心下面那个函数,其中amountIn是100,所以amountInWithFee是99700,reserveOut和reserveIn都是4000(上面lpUser两次添加交易对),最终的amountOut=(99700*4000)/(4000*1000+99700) = 97.27,也就是当前交易对100DAI可以换取97.27的USDT,而按照当前DAI:USDT=1:1,应该获取100USDT啊,那么剩下的USDT哪去了呢?其实剩下的USDT留在了UniswapV2Pair在USDT的账户里,这就是交易所产生的手续费。随着交易不断进行,UniswapV2Pair在USDT的账户会产生越来越多的USDT,这些USDT就是DAI/USDT交易对的流动性提供者获取的收益。

拿到USDT的数量后,UniswapV2Router02会先将100DAI从swapUser的账户转到UniswapV2Pair的账户,然后调用_swap进行真在的交换:

function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {for (uint i; i < path.length - 1; i++) {(address input, address output) = (path[i], path[i + 1]);(address token0,) = UniswapV2Library.sortTokens(input, output);uint amountOut = amounts[i + 1];(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(amount0Out, amount1Out, to, new bytes(0));}}

这里input其实是token0,也就是DAI,所以amount0Out为0,amount1Out是97.27,to是swapUser在USDT合约的地址

然后执行UniswapV2Pair的swap函数

function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savingsrequire(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');uint balance0;uint balance1;{ // scope for _token{0,1}, avoids stack too deep errorsaddress _token0 = token0;address _token1 = token1;require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokensif (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokensif (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);balance0 = IERC20(_token0).balanceOf(address(this));balance1 = IERC20(_token1).balanceOf(address(this));}uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');{ // scope for reserve{0,1}Adjusted, avoids stack too deep errorsuint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');}_update(balance0, balance1, _reserve0, _reserve1);emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);}

这段代码首先把97.27的USDT转到了to地址,然后获取balance0=4100(UniswapV2Pair持有的DAI的数量),获取balance1=3902.73(UniswapV2Pair持有的USDT的数量)。

计算amount0In,由于_reserve0-amount0Out=4000-0,balance0>_reserve0-amount0Out,所以amount0In=4100-4000+0=100

计算amount1In,由于_reserve1-amount1Out=4000-97.27,balance1=_reserve1-amount1Out,所以amount1In = 0

计算balance0Adjusted = 4100*1000 - 100*3 = 4100000-300 = 4099700

计算balance1Adjusted=3902.73*1000-0 = 3902730

balance0Adjusted*balance1Adjusted = 16000022.181 * 1000* 1000   >   _reserve0*_reserve1*1000*1000 = 16000000 * 1000 * 1000

最后更新_reserve0和_reserve1

5、lpUser提取一半流动性

调用是从UniswapV2Router02的removeLiquidity开始的:

function removeLiquidity(address tokenA,address tokenB,uint liquidity,uint amountAMin,uint amountBMin,address to,uint deadline) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');}

这里先获取DAI/USDT交易对UniswapV2Pair,然后把一半流动性(2000)转移到UniswapV2Pair,然后调用UniswapV2Pair的burn函数销毁流动性并取回DAI和USDT:

function burn(address to) external lock returns (uint amount0, uint amount1) {(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savingsaddress _token0 = token0;                                // gas savingsaddress _token1 = token1;                                // gas savingsuint balance0 = IERC20(_token0).balanceOf(address(this));uint balance1 = IERC20(_token1).balanceOf(address(this));uint liquidity = balanceOf[address(this)];bool feeOn = _mintFee(_reserve0, _reserve1);uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFeeamount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distributionamount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distributionrequire(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');_burn(address(this), liquidity);_safeTransfer(_token0, to, amount0);_safeTransfer(_token1, to, amount1);balance0 = IERC20(_token0).balanceOf(address(this));balance1 = IERC20(_token1).balanceOf(address(this));_update(balance0, balance1, _reserve0, _reserve1);if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-dateemit Burn(msg.sender, amount0, amount1, to);}

首先获取UniswapV2Pair持有的DAI数量,balance0=4100

然后获取UniswapV2Pair持有的USDT数量,balance1=3902.73

然后获取lpUser转移过来的流动性,liquidity=2000

然后计算手续费:

function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {address feeTo = IUniswapV2Factory(factory).feeTo();feeOn = feeTo != address(0);uint _kLast = kLast; // gas savingsif (feeOn) {if (_kLast != 0) {uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));uint rootKLast = Math.sqrt(_kLast);if (rootK > rootKLast) {uint numerator = totalSupply.mul(rootK.sub(rootKLast));uint denominator = rootK.mul(5).add(rootKLast);uint liquidity = numerator / denominator;if (liquidity > 0) _mint(feeTo, liquidity);}}} else if (_kLast != 0) {kLast = 0;}}

这里rootK = 4100*3902.73的开根号,也就是4000.149

由于上一次交易的时候_kLast是没有重算的,所以rootKLast = 4000

rootK其实是大于rootKLast的,所以需要计算手续费:

numerator = 4000*(4000.149-4000)=596

denominator=4000.149*5+4000 = 24000.745

liquidity = 596/24000.745 = 0.0248

然后将手续费交给feeTo地址

计算完手续费后,继续执行burn函数:

        uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFeeamount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distributionamount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distributionrequire(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');_burn(address(this), liquidity);_safeTransfer(_token0, to, amount0);_safeTransfer(_token1, to, amount1);balance0 = IERC20(_token0).balanceOf(address(this));balance1 = IERC20(_token1).balanceOf(address(this));_update(balance0, balance1, _reserve0, _reserve1);if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-dateemit Burn(msg.sender, amount0, amount1, to);

amount0 = 2000*4100/4000.0248 = 2049.987

amount1 = 2000*3902.73/4000.0248 = 1951.352

调用_burn函数销毁2000流动性

将2049.987个DAI和1951.352个USDT转给lpUser(如果DAI和USDT的价格都是1美元,那么用户实际获得的流动性收益为2049.987+1951.352-4000 = 1.339)

更新_reserve0和_reserve1,并重新计算kLast

6、总结

a)、添加和移除流动性,会去算手续费,发给相应地址,然后更新kLast、reserve0、reserve1

b)、交易的时候,会按比例留下一部分币,这些币就是流动性提供者获取的收益,交易只会更新reserve0和reserve1,不会更新kLast

c)、添加流动性-->交易--->提取流动性,整个操作过后,kLast其实是有一点点变化的,并不是严格保持一致。

Uniswap计算过程推演相关推荐

  1. 无人驾驶运动学模型——线性时变模型预测控制的思路推演过程

    无人驾驶运动学模型--线性时变模型预测控制的思路推演过程 在做MPC仿真时,有些代码看不太懂,就去重翻了龚建伟的<无人驾驶车辆模型预测控制>这本书,怎么说呢,这么说吧,应该说它是目前为止讲 ...

  2. 无人驾驶运动学模型——线性时变模型预测控制的思路推演过程_百叶书的博客-CSDN博客_线性时变模型预测控制 转

    无人驾驶运动学模型--线性时变模型预测控制的思路推演过程_百叶书的博客-CSDN博客_线性时变模型预测控制

  3. 卫星轨道推演计算相关知识点总结(含欧拉角、旋转矩阵、及各坐标系转化等)

    来源:轨道机动算法的C++实现_shirro123的专栏-CSDN博客 卫星轨道推演计算相关基础知识点总结 一.卫星的运动特性             二.卫星的空间坐标系               ...

  4. Android 系统(269)---native保活5.0以上方案推演过程以及代码详述

    Android 进程常驻(4)----native保活5.0以上方案推演过程以及代码详述 这是一个轻量级的库,配置几行代码,就可以实现在android上实现进程常驻,也就是在系统强杀下,以及360获取 ...

  5. Android 系统(268)---native保活5.0以下方案推演过程以及代码详述

    Android 进程常驻(3)----native保活5.0以下方案推演过程以及代码详述 这是一个轻量级的库,配置几行代码,就可以实现在android上实现进程常驻,也就是在系统强杀下,以及360获取 ...

  6. LC滤波器之m推演滤波器的LC参数matlab计算

    参考森荣二<LC滤波器设计与制作>书籍. 不同于定K型滤波器,m推演型LC滤波器可以设置陷波点用来滤除截止频率附近的频段. 其参数计算公式如下: 手按计算器算无论是直接计算还是利用标准单元 ...

  7. 深度长文 | 从FM推演各深度CTR预估模型(附开源代码)

    作者丨龙心尘 & 寒小阳 研究方向丨机器学习,数据挖掘 题记:多年以后,当资深算法专家们看着无缝对接用户需求的广告收入节节攀升时,他们可能会想起自己之前痛苦推导 FM 与深度学习公式的某个夜晚 ...

  8. 从FM推演各深度学习CTR预估模型

    本文的PDF版本.代码实现和数据可以在我的github取到. 1.引言 点击率(click-through rate, CTR)是互联网公司进行流量分配的核心依据之一.比如互联网广告平台,为了精细化权 ...

  9. CV学习笔记-BP神经网络训练实例(含详细计算过程与公式推导)

    BP神经网络训练实例 1. BP神经网络 关于BP神经网络在我的上一篇博客<CV学习笔记-推理和训练>中已有介绍,在此不做赘述.本篇中涉及的一些关于BP神经网络的概念与基础知识均在< ...

最新文章

  1. 母亲,我怎么让妳等了那么久...
  2. linux——用户理解及管理
  3. 中国学霸们被世界名校集体退学,原因竟然是……
  4. C++什么是内存泄漏
  5. java文件处理之压缩,分割
  6. 【Flask模板】include标签
  7. svg defs 进行定义 引用
  8. 十、request.getSession( )、reqeust.getSession(false)和 request.getSession(true)
  9. hdu4825 字典树 XOR
  10. sql always on_Always On可用性组中具有发布者数据库SQL复制
  11. Android源码分析(三)-----系统框架设计思想
  12. mui多层tab切换上拉加载的实现
  13. C#实现对Access数据库的通用操作
  14. Third season fifth episode,Phoebe‘s brother Frank came to see her
  15. 芯片设计进阶之路——Reset深入理解
  16. 第二类曲线、曲面积分计算公式
  17. java水平翻转矩阵_Java实现 LeetCode 519 随机翻转矩阵
  18. 如何在电脑上打开epub电子书
  19. 政策解读|2023法定节假日安排发布了,HR需要跟进的三件事
  20. 架构师如何设计架构,架构师的架构思路

热门文章

  1. 150万奖金:首届6G智能无线通信系统大赛正式上线
  2. Java的反射机制 —— 类的镜子
  3. 空心等腰三角形 (15 分)
  4. 外校保研北大计算机,北大2018年本校保研率超53% 外校生多来自双一流名校
  5. 1.1什么是计算机网络
  6. React 报错与解决方法
  7. 【题解】P4516 [JSOI2018] 潜入行动
  8. 2012年百度实习生招聘——java开发
  9. 关于手势(Gesture)
  10. zemax 非序列模式 双折射晶体