Ceres求解優化問題

1. 簡介

Ceres Solver是專門用于求解非線性最小二乘問題的C++開源庫,研究SLAM方向不過濾波和優化兩個技術路線,因此常用Ceres庫解決實際項目中的優化問題,當然還有g2o同樣可用,但就說明文檔而言,Ceres對新用戶更友好,g2o提供不多的文檔,更多是需要參考其它開源項目使用,所以筆者目前更加喜歡使用Ceres.
本文更多的站在一個SLAMer的角度講解Ceres的基本使用方法,如需要了解更多細節功能,請直接跳轉下面的官方文檔:

  • Ceres官方文檔-英文
  • Ceres說明文檔-中文

2. 基本使用流程

2.1 Problem

首先我們使用Ceres目的是解決最小二乘問題,所以必須創建一個最小乘問題的對象

ceres::Problem problem;

然后,要定義一個最小二乘問題無非就是需要兩部分內容:

  • 待優化變量
  • 誤差(cost function)

對應于圖優化其實就是圖頂點和邊,這是使用Ceres最關鍵的部分.

2.2 Cost Function

添加邊在Ceres中就是添加cost function.首先我們需要定義一個Cost Function類.這時候你需要考慮一個問題,自己推導雅可比矩陣還是使用Ceres的自動推導:

  • 自動求導
  • 解析導數

2.2.1 使用自動求導的Cost Function

自動導數只需提供誤差的計算方式即可

struct ExponentialResidual {// 提供一個構造函數方便傳入私有成員的初值ExponentialResidual();// 重載():Ceres會默認這個()函數計算誤差template <typename T>bool operator()(const T* const m, const T* const c, T* residual) const {residual[0] = y_ - exp(m[0] * x_ + c[0]);return true;}private:// 存放計算誤差時需要的私有成員
};

細心的讀者已經發現一個細節,重載的operator()是一個模板函數,Ceres正是使用這個模板類完成自動求導,其實現非常妙~關于自動求導的實現請往下閱讀.
對于使用自動求導的cost Function到此已經完成定義,下一步只需要添加到問題中即可:

double m = 0.0;
double c = 0.0;Problem problem;
for (int i = 0; i < kNumObservations; ++i) {CostFunction* cost_function =new AutoDiffCostFunction<ExponentialResidual, 1, 1, 1>(data[2 * i], data[2 * i + 1]);problem.AddResidualBlock(cost_function, nullptr, &m, &c);
}

這里解析一下AutoDiffCostFunction的模板參數:

  • 第一個模板參數為誤差的維度
  • 后續第n+1個模板參數分別對應第n個頂點(待優化變量)的維度

最后便是使用AddResidualBlock添加Cost Function即可.

2.2.2 使用解析導數的Cost Function

如果使用解析導數的方式,定義Cost Function時除了需要提供誤差的計算方式,當然還需要告知Ceres誤差關于待優化變量的雅可比計算方式.具體使用時需要構造一個繼承SizedCostFunction的子類,并重載Evaluate,Evaluate將提供返回誤差結果和雅可比結果,下面是官方的使用例子:

class Rat43Analytic : public SizedCostFunction<1,4> {public:Rat43Analytic(const double x, const double y) : x_(x), y_(y) {}virtual ~Rat43Analytic() {}virtual bool Evaluate(double const* const* parameters,double* residuals,double** jacobians) const {const double b1 = parameters[0][0];const double b2 = parameters[0][1];const double b3 = parameters[0][2];const double b4 = parameters[0][3];residuals[0] = ...;if (!jacobians) return true;double* jacobian = jacobians[0];if (!jacobian) return true;jacobian[0] = ...;jacobian[1] = ...;jacobian[2] = ...;jacobian[3] = ...;return true;}private:const double x_;const double y_;};

值得注意的是:

  1. SizedCostFunction的模板函數和自動求導中的AutoDiffCostFunction類似,第一個模板參數對應誤差的維度,后面第n個分別對應第n個頂點的維度;
  2. 注意jacobians指針的指針,jacobians表示指向所有頂點參數數組的地址,jacobians[i]表示指向第i個頂點參數數組的地址,jacobians[i][j]表示第i個頂點參數數組的第j個參數.注意上面的寫法,要對jacobiansjacobians[i]加以判斷,這說明Ceres在調用這個Evaluate成員函數時并一定需要計算雅可比,不加這個判斷會導致程序段錯誤.

最后同樣只需要添加至problem即可

param[4] = {b1, b2, b3, b4};
ceres::CostFunction* cost_function = new Rat43Analytic(x, y);
problem.AddResidualBlock(cost_function, nullptr, param);

2.3 待優化變量

不同于g2o,Ceres在實現上沒有將頂點和邊完全分離,具體而言是,在定義邊時已經關聯了頂點,一般情況無須自己定義頂點.但是Ceres考慮到用戶不同的需求,比如SLAM中常見的優化位姿問題,不同于普通向量空間,李群不能使用普通加減法,且由于自由度冗余不適合直接用于優化,因此Ceres向用戶提供了LocalParameterization.

class PoseSE3Parameterization: public ceres::LocalParameterization {
public:PoseSE3Parameterization() {}virtual ~PoseSE3Parameterization() {}virtual int GlobalSize();virtual int LocalSize();virtual bool Plus(const double* x, const double* delta, double* x_plus_delta) const;virtual bool ComputeJacobian(const double* x, double* jacobian) const;
};

使用LocalParameterization最基本需要重載4個成員方法:

  1. GlobalSize需返回全局空間(不知道這么表達是否正確)的維度,對于SO3一般使用李代數表示,所以是4,對于SE3就是7,;
  2. LocalSize需返回局部空間的維度,對于位姿就是流型的切空間維度,即對于so3的維度是3,對于se3的維度6;
  3. Plus需提供全局空間的加法操作,上面提到之所以使用LocalParameterization就是因為這類待優化變量不能使用普通的加法,所以這一步告知Ceres如何對變量進行更新;
  4. ComputeJacobian需提供全局空間關于局部空間的雅可比矩陣,不難猜測Ceres進行優化時先調用誤差中Evaluate計算誤差關于全局空間的雅可比,再調用這里的ComputeJacobian得到全局空間關于局部空間的雅可比,根據鏈式法則兩個直接相乘得到的就是誤差關于局部空間的雅可比.

這里先簡單介紹自定義待優化變量的使用,后文將以SLAM中常見的位姿優化問題為例介紹如何編寫PlusComputeJacobian.

到此已經自定義完成待優化變量,后面同樣只需要將其添加到Problem即可,如

problem.AddParameterBlock(param, 7, new PoseSE3Parameterization());

上面的7自然代表就是SE3的維度.

2.4 線性求解器

現在定義好了誤差和待優化變量,即確定了最小二乘問題,理論上可以進行優化了,但事實上還需要解決一個實際計算的問題,那便是如何求解線性方程:
A x = b \boldsymbol{A}\boldsymbol{x} = \boldsymbol{b} Ax=b
顯然求 A \boldsymbol{A} A 的逆是最直接的方法,但直接求逆是十分低效的手段;另外SLAM的優化問題一般具有稀疏性,為了更高效地求解上面的線性方程組,Ceres提供了多個選項.這里就直接引用博客:ceres solver里的線性求解器的解釋:

  1. DENSE_QR: 用于小規模最小二乘問題的求解
  2. DENSE_NORMAL_CHOLESKY&SPARSE_NORMAL_CHOLESKY:cholesky分解,用于具有稀疏性的大規模非線性最小二乘問題求解
  3. DENSE_SCHUR&SPARSE_SCHUR: SCHUR分解,用于BA問題求解
  4. ITERATIVE_SCHUR: 使用共軛梯度schur求解BA問題
  5. CGNR: 使用共軛梯度法求解稀疏方程

用戶只需要通過ceres::Solver::Options選擇哪種求解器即可,例如使用DENSE_QR:

ceres::Solver::Options options;
options.linear_solver_type = ceres::DENSE_QR;

筆者目前而言沒有深入了解上面各個求解方法在實際應用中所帶來差異,但想在此強調的是要根據實際的優化問題合理選擇所需的求解器,例如SLAM中的Bundle Adjustment問題中 A \boldsymbol{A} A 具有明顯的稀疏性,使用SCHUR消元再求解可以極大提高計算效率,具體詳看<視覺SLAM十四講-ch10>.

2.5 執行求解

最后只需要將上面構建的最小二乘Problem和配置好的Option傳入Solve函數即可,另外可以輸出優化信息,具體如下:

ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
std::cout << summary.FullReport() << std::endl;

2.6 其它

2.6.1 損失函數

上面例子中AddResidualBlock的第二個參數為nullptr即使用默認的二范數作為損失函數,為了避免一些異常值對優化的影響可以選用其它損失函數,例如:

problem.AddResidualBlock(cost_function, new CauchyLoss(0.5), param);

2.6.2 堆內存

細心的讀者可能發現,上面的實例代碼中邊和頂點都使用了堆內存來實例化,那計算完之后還需要主動delete嗎?看官方的例程后結論是不需要,說明Ceres::Problem自己在析構時會對其內存進行釋放.

2.7 小結

上面講解了如何使用Ceres解決一個實際的優化問題,根據以上的步驟能夠解決大多數的問題.然而,對于位姿到底如何自定義LocalParameterization?自動求導又為何看起來如此"智能"?下面繼續深入一點討論.

3. 使用Ceres實現位姿優化

3.1 重投影誤差

上面提到對于位姿這種流型空間的優化問題,以重投影誤差(看到這里我相信讀者也是做SLAM或相關,重投影誤差就不再多說了)為例介紹其邊的定義,待優化變量使用了四元數加位移的方法表達SE3,其中四元數放在前面,即 [ q , t ] T [q, t]^T [q,t]T, 增量se3則是 [ ? , ρ ] T [\phi, \rho]^T [?,ρ]T

bool ReprojectionSe3::Evaluate(double const *const *parameters, double *residuals, double **jacobians) const
{Eigen::Map<const Eigen::Quaterniond> q_cw(parameters[0]);Eigen::Map<const Eigen::Vector3d> t_cw(parameters[0] + 4);Eigen::Vector3d p_c = q_cw * mP3D + t_cw;double z_inv = 1.0 / p_c.z();residuals[0] = mP2D.x - mK.fx * p_c.x() * z_inv - mK.cx;residuals[1] = mP2D.y - mK.fy * p_c.y() * z_inv - mK.cy;if(jacobians) {if(jacobians[0]) {double z2_inv = z_inv * z_inv;Eigen::Map<Eigen::Matrix<double, 2, 7, Eigen::RowMajor> > J_se3(jacobians[0]);J_se3.setZero();J_se3(0, 0) = mK.fx * p_c.x() * p_c.y() * z2_inv;J_se3(0, 1) =-mK.fx * (1 + p_c.x() * p_c.x() * z2_inv);J_se3(0, 2) = mK.fx * p_c.y() * z_inv;J_se3(0, 3) =-mK.fx * z_inv;J_se3(0, 4) = 0.0;J_se3(0, 5) = mK.fx * p_c.x() * z2_inv;J_se3(1, 0) = mK.fy * (1 + p_c.y() * p_c.y() * z2_inv);J_se3(1, 1) =-mK.fy * p_c.x() * p_c.y() * z2_inv;J_se3(1, 2) =-mK.fy * p_c.x() * z_inv;J_se3(1, 3) = 0.0;J_se3(1, 4) =-mK.fy * z_inv;J_se3(1, 5) = mK.fy * p_c.y() * z2_inv;}}return true;
}

雅可比的計算和<視覺SLAM14講>一模一樣,唯一不同是這里由于旋轉放在位移的前面,所以矩陣的前3列和后3列調換了.

3.2 重載自定義變量的成員方法

1.維度成員方法

int PoseSE3Parameterization::GlobalSize() const { return 7; }
int PoseSE3Parameterization::LocalSize() const { return 6; }

GlobalSizeLocalSize正如前面提到的分別對應SE3的維度和se3點維度,所以分別是7和6.

2.待優化變量更新成員方法

bool PoseSE3Parameterization::Plus(const double *x, const double *delta, double *x_plus_delta) const
{Eigen::Map<const Eigen::Vector3d> trans(x + 4);Eigen::Quaterniond delta_q;Eigen::Vector3d delta_t;GetTransformFromSe3(Eigen::Map<const Eigen::Matrix<double,6,1>>(delta), delta_q, delta_t);Eigen::Map<const Eigen::Quaterniond> quater(x);Eigen::Map<Eigen::Quaterniond> quater_plus(x_plus_delta);Eigen::Map<Eigen::Vector3d> trans_plus(x_plus_delta + 4);quater_plus = delta_q * quater;trans_plus = delta_q * trans + delta_t;return true;
}

Plus操作提供了SE3點更新方法,注意:

  • x:應的是7維的SE3,即 [ q , t ] T [q, t]^T [q,t]T;
  • delta就是解線性方程后得到的se3增量, 即 [ ? , ρ ] T [\phi, \rho]^T [?,ρ]T;
  • x_plus_delta對應更新后的SE3;
  • GetTransformFromSe3實現了se3到SE3指數映射過程,具體可以參考F-LOAM

不難發現上面實則實現的左乘的更新操作:
T K + 1 = Δ T ? T k \boldsymbol{T}_{K+1} = \Delta\boldsymbol{T} * \boldsymbol{T}_k TK+1?=ΔT?Tk?

3.待優化變量的全局關于局部的雅可比成員方法:

bool PoseSE3Parameterization::ComputeJacobian(const double *x, double *jacobian) const
{Eigen::Map<Eigen::Matrix<double, 7, 6, Eigen::RowMajor>> j(jacobian);(j.topRows(6)).setIdentity();(j.bottomRows(1)).setZero();return true;
}

看到這里我想大部分人都會疑惑,這一步似乎啥都沒做啊?
的確,這里根本沒做任何"有用"的操作,只是將前6維取出來,這是因為前面邊的雅可比Evaluate中已經完成了誤差關于se3點求導計算,但是維度是2x7,我們只需要在這一步取出前6維即可得到最終的2x6維度.
或許有人會質疑,為啥實現得如此不直觀?
事實上筆者已經從好幾個優秀的開源項目中看到過類似的實現,包括上面的實現其實出自F-LOAM;另一個方面,筆者作為從<視覺SLAM14講>入門的學生來說,直接對se3求導似乎更容易,反而要讓筆者計算關于四元數的雅可比會令我不知所措.
當然按照Ceres設計那樣分別提供 誤差關于SE3點雅可比 以及 SE3關于se3的雅可比 也絕對沒問題,想要了解該實現的歡迎跳轉筆者另一篇博客Ceres姿態優化.

4. 自動求導的背后原理

自動求導的理論依據是引入了二元數(Jet),可以將其理解為一個復數,有一個無窮小的分量:
x = a + δ x = a + \delta x=a+δ
以平方操作為例:
f ( x ) = x 2 = ( a + δ ) 2 = a 2 + 2 a δ + δ 2 f(x) = x^2 = (a+\delta)^2 = a^2 + 2a\delta + \delta^2 f(x)=x2=(a+δ)2=a2+2aδ+δ2
只保留一階無窮小則
f ( x ) ≈ a 2 + 2 a δ f(x) \approx a^2 + 2a\delta f(x)a2+2aδ
可見 2 a 2a 2a 正是 f ( x ) f(x) f(x) x = a x = a x=a 處的導數.不加證明地說(事實這個有點類似擾動,它的思想就是代入一個微小的擾動,取其系數代表該擾動對函數的影響程度),這個方法對于其它操作同樣適用,那么只要我們實現了二元數的各種運算操作(包括加減乘除指數冪)即可對大部分計算進行自動求導.
Ceres正是實現了二元數(Jet)這一種類:

template<int N> struct Jet {double a;Eigen::Matrix<double, 1, N> v;
};template<int N> Jet<N> operator+(const Jet<N>& f, const Jet<N>& g) {return Jet<N>(f.a + g.a, f.v + g.v);
}template<int N> Jet<N> operator-(const Jet<N>& f, const Jet<N>& g) {return Jet<N>(f.a - g.a, f.v - g.v);
}template<int N> Jet<N> operator*(const Jet<N>& f, const Jet<N>& g) {return Jet<N>(f.a * g.a, f.a * g.v + f.v * g.a);
}template<int N> Jet<N> operator/(const Jet<N>& f, const Jet<N>& g) {return Jet<N>(f.a / g.a, f.v / g.a - f.a * g.v / (g.a * g.a));
}template <int N> Jet<N> exp(const Jet<N>& f) {return Jet<T, N>(exp(f.a), exp(f.a) * f.v);
}// This is a simple implementation for illustration purposes, the
// actual implementation of pow requires careful handling of a number
// of corner cases.
template <int N>  Jet<N> pow(const Jet<N>& f, const Jet<N>& g) {return Jet<N>(pow(f.a, g.a),g.a * pow(f.a, g.a - 1.0) * f.v +pow(f.a, g.a) * log(f.a); * g.v);
}

自動求導的實現相當巧妙,但凡事有兩面性,自動求導也有缺點:

  • 解析導數可以優化計算過程提高效率,而自動求導可能計算時含有重復的計算導致效率低一點;
  • 自動求導不能涵蓋所有特殊情況,特殊情況需要特殊處理,比如SO3的計算不能直接如此使用(不過Ceres考慮得比較周全,提供了一個ceres::AngleAxisRotatePoint接口解決問題).

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/16429.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/16429.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/16429.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【JAVA】接口

前面我們說了說抽象類相關內容&#xff0c;這篇我們主要聊聊接口相關內容&#xff0c;這部分很重要&#xff0c;大家引起關注。 1. 接口 1.1 接口的概念 接口就是公共的行為規范標準&#xff0c;大家在實現時&#xff0c;只要符合規范標準&#xff0c;就可以通用。在Java中&am…

力扣 739. 每日溫度 python AC

單調棧 class Solution:def dailyTemperatures(self, temperatures):size len(temperatures)ll []ans [0] * sizefor i in range(size - 1, -1, -1):while ll and temperatures[i] > temperatures[ll[-1]]:ll.pop()if ll:ans[i] ll[-1] - ill.append(i)return ans

C語言 數組——向函數傳遞數組

目錄 把數組傳給函數&#xff08;Passing Arrays to Functions&#xff09; 向函數傳遞一維數組 向函數傳遞二維數組 數組在學生成績管理中的應用 例&#xff1a;計算每個學生的平均分 把數組傳給函數&#xff08;Passing Arrays to Functions&#xff09; 向函數傳遞一維…

gnocchi學習小結

背景 總結gnocchi 4.4版本gnocchi-metricd工作流程 入口 gnocchi.cli.metricd metricd stop after processing metric默認為0&#xff0c;調servicemanager run MetricdServiceManager __init__ 服務邏輯封裝到MetricdServiceManager初始化中 主要由MetricProcessor, Met…

基于Vue的前端自定義詢問彈框與輸入彈框組件的設計與實踐

基于Vue的前端自定義詢問彈框與輸入彈框組件的設計與實踐 摘要 隨著技術的不斷進步&#xff0c;前端開發面臨越來越多的挑戰&#xff0c;其中之一就是如何有效管理復雜的業務邏輯和用戶體驗。傳統的整塊應用開發方式在面對頻繁的功能變更和用戶體驗優化時&#xff0c;往往顯得…

python數據分析-CO2排放分析

導入所需要的package import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import datetime %matplotlib inline plt.rcParams[font.sans-serif] [KaiTi] #中文 plt.rcParams[axes.unicode_minus] False #負號 數據清洗…

MySQL數據表索引命名規范

在數據庫設計和開發過程中&#xff0c;索引是提高查詢性能的重要工具。合理的索引命名規范不僅能提高代碼的可讀性&#xff0c;還能便于維護和管理。本文將詳細介紹MySQL數據表索引的命名規范&#xff0c;包括不同類型索引的命名方法&#xff0c;并提供多個代碼示例以說明如何命…

SSH 遠程登錄系統和遠程拷貝

文章目錄 目錄 文章目錄 前言 一.SSH的基本用法 SSH基本用法&#xff1a; SSH無密碼登錄 二.SSH安全設置 三.SSH限制用戶 前言 很多時候服務器并沒有服務器&#xff0c;我們也不能每次都通過控制臺去管理服務器&#xff0c;這時候就需要遠程登錄&#xff0c;相比于Telnet&a…

京東應屆生公司內網說了一句‘什么時候被pdd收購‘,結果慘遭辭退

京東應屆生公司內網說了一句’什么時候被pdd收購’&#xff0c;結果慘遭公司開除 這個事最近在圈子討論比較多 前二天&#xff0c;有一個上海交大畢業的應屆生&#xff0c;在京東實習了9個月&#xff0c;好不容易轉正12天后&#xff0c;只因在內網說了一句話&#xff0c;就被…

upload-labs 21關解析

目錄 一、代碼審計 二、實踐 三、總結 一、代碼審計 $is_upload false; $msg null; if(!empty($_FILES[upload_file])){//檢查MIME$allow_type array(image/jpeg,image/png,image/gif);if(!in_array($_FILES[upload_file][type],$allow_type)){$msg "禁止上傳該類型…

一個程序員的牢獄生涯(38)答案

星期一 答 案 我被這個不知道什么時候無聲無息的出現在身后的人嚇出了一身的冷汗。 看到我發現了他,這個人慢慢地抬起了頭……“他X的,是小X州!” 此時的小X州臉上并沒有著急等待上廁所的表情,反而是用一種狡黠的眼神看著我。一直充滿的敵意,現在又多了一絲威脅的神情,讓…

Quartus Cyclone I II III IVE 器件型號

玩耍了一個 EP2 型號的開發板&#xff0c;發現 安裝的quartus13 沒有Cyclone II 型號&#xff0c;經過探索發現了是版本不對。 https://www.intel.com/content/www/us/en/software-kit/711920/intel-quartus-ii-subscription-edition-design-software-version-13-0sp1-for-win…

行業分析---造車新勢力之蔚來汽車

1 前言 在之前的博客中&#xff0c;筆者分析了蘋果《行業分析---我眼中的Apple Inc.》&#xff0c;蘋果已經成為世界級的公司。隨后也分析了電動汽車公司特斯拉《行業分析---馬斯克的Tesla》&#xff0c;特斯拉也在不斷成長。目前能分析的新能源汽車公司不多&#xff0c;小米汽…

Minecraft服務器如何搭建

Minecraft這是原版英文名稱&#xff0c;在中國大陸被譯為《我的世界》&#xff0c;這款游戲很火爆。臺灣的很多小伙伴也在玩&#xff0c;其譯名為《我的創世神》。現在這款游戲在國內已經被網易代理了。因為這款游戲開源&#xff0c;所以任何人都可以搭建服務器端&#xff0c;如…

機器人支持回調接口配置(詳細教程)

大家伙&#xff0c;我是雄雄&#xff0c;歡迎關注微信公眾號&#xff1a;雄雄的小課堂。 一、前言 今天&#xff0c;給大家介紹一下&#xff0c;如何在機器人中配置回調地址和接口編寫。很多時候我們可能有這樣的場景&#xff0c;收到消息后&#xff0c;想自己處理一下消息的內…

【Linux】Linux的基本指令_2

文章目錄 二、基本指令8. man9. nano 和 cat10. cp11. mv12. echo 和 > 和 >> 和 <13. more 和 less14. head 和 tail 和 | 未完待續 二、基本指令 8. man Linux的命令有很多參數&#xff0c;我們不可能全記住&#xff0c;我們可以通過查看聯機手冊獲取幫助。訪問…

基于門控的循環神經網絡:GRU

門控循環單元&#xff08;GatedRecurrentUnit&#xff0c;GRU&#xff09;網絡&#xff0c;也是一種基于門控的循環神經網絡&#xff0c;但是名氣不如LSTM大&#xff0c;GRU是對LSTM的一種改版&#xff0c;可以理解為是LSTM的簡化版。LSTM有三個門&#xff0c;輸入門&#xff0…

【C++】牛客 ——DP36 abb

?題目鏈接&#xff1a; DP36 abb ?題目描述 leafee 最近愛上了 abb 型語句&#xff0c;比如“疊詞詞”、“惡心心” leafee 拿到了一個只含有小寫字母的字符串&#xff0c;她想知道有多少個 "abb" 型的子序列&#xff1f; 定義&#xff1a; abb 型字符串滿足以下…

perl:用 Net::Server 創建簡單的流媒體服務器

這是一個使用Perl Net::Server 模塊創建的簡單流媒體服務器示例&#xff0c;它能夠播放.flv文件。 首先&#xff0c;確保安裝了Net::Server模塊&#xff0c;如果沒有安裝&#xff0c;可以使用CPAN來安裝它&#xff1a; 運行 cpan Net::Server RHANDOM/Net-Server-2.014.tar.…

力扣刷題--448. 找到所有數組中消失的數字【簡單】

題目描述 給你一個含 n 個整數的數組 nums &#xff0c;其中 nums[i] 在區間 [1, n] 內。請你找出所有在 [1, n] 范圍內但沒有出現在 nums 中的數字&#xff0c;并以數組的形式返回結果。 示例 1&#xff1a; 輸入&#xff1a;nums [4,3,2,7,8,2,3,1] 輸出&#xff1a;[5,6…