哈喽大家好,我是 doooge,今天给大家带来的是 区间DP 的详解。

\[\Huge \sf 区间DP 详解
\]

1.什么是区间DP

区间DP 是动态规划的一部分。

1.1 dp[i][j] 的含义

所谓 区间DP,也就是 \(dp\) 数组 \(dp_{i,j}\) 表示的是一个区间的答案,比如说 \(dp_{2,5}\) 表示的就是 \([2,5]\) 这个区间的答案,\(dp_{3,7}\) 表示的就是 \([3,7]\) 这个区间的答案。

而且,\(dp_{i,j}\) 只能由它的子区间转移而来,也就像下图这样:

是不是很简单?那我们应该用怎样的顺序去遍历 \(i,j\) 使得在 \([i,j]\) 的子区间一定在遍历到 \([i,j]\) 前全部推完呢?

1.2 区间DP的遍历方式

我们可以枚举长度遍历。比如说,我们在遍历到 \(i,j\) 时,长度在 \(j-i+1\) 以下的区间一定会被遍历掉,可以看下图理解:

遍历的代码也很好写:

for(int l=1;l<=n;l++){//遍历长度
for(int i=1;i<=n-l+1;i++){//枚举左端点
int j=i+l-1;//求出右端点
//dp[i][j]=...
}
}

1.3 DP推出的结果在哪

这个其实很好猜,根据我们以往的经验,一维 \(dp\) 的答案一般是最后遍历到的第 \(n\) 位 \(dp_n\),那么区间DP 的答案应该就是我们最后遍历到的区间,也就是长度最长的区间 \(dp_{1,n}\) 这里。

当然,也有特例,当题目要你求所有状态之和时,这时答案就不太可能是 \(dp_{1,n}\) 了,而是每个区间的答案之和,用公式表达就是:

\[\sum_{l=1}^{n} \sum_{i=1}^{n-l+1} dp_{i,i+l-1}
\]

也可以是:

\[\sum_{i=1}^{n} \sum_{j=i+1}^{n} dp_{i,j}
\]

当然这两个求和公式的本质都是一样的,遍历每个区间,只是遍历方式的不同而已。

2.区间dp的例题

2.1 T1:P1775 石子合并(弱化版)

有 \(N\) 堆石子排成一排,每堆石子有一定的质量 \(m_i\)。现在要将这 \(N\) 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。试找出一种合理的方法,使总的代价最小,并输出最小代价。

\(N \le 300\),\(m_i \le 1000\)。

2.1.1 思路:

我们先把样例搬过来模拟一下:

输入 #1

4
2 5 3 1

输出 #1

22

我们可以按以下顺序合并:

我们发现,不管怎么合并,\([i,j]\) 合并出来的石子堆的大小都是一样的,也就是,不管怎么合并,这一步合并的方法跟后续的合并没有影响。

比如说,这里有四堆石子 \(a,b,c,d\),我们用两个方法先合并 \(b,c,d\) 这 \(3\) 堆石子,再去合并第 \(4\) 堆石子 \(a\),情况是这样的:

于是!我们就可以用 \(dp_{i,j}\) 表示合并 \([i,j]\) 这几堆石子合并到一起所需的最小代价。那么我们最后的答案就是 \(dp_{1,n}\) 了。

那我们的 \(dp_{i,j}\) 应该从哪里转移过来呢?我们可以想象 \([i,j]\) 这堆石子怎么合并过来的。

诶!我们发现,\([i,j]\) 这堆石子是通过包含在 \([i,j]\) 里面的两小堆石子合并而来的,那么我们的 \(dp_{i,j}\) 应该也是找一个断点 \(k\),从 \(dp_{i,k}\) 和 \(dp_{k+1,j}\) 再加上合并的代价得到的,也就是这样:

而 \(m_i + m_{i+1} + \cdots + m_j\) 可以用前缀和记录(记为 \(s\) 数组),于是 \(dp_{i,j}\) 就等于:

\[\min_{k=i}^{j-1} dp_{i,k}+dp_{k+1,j}+s_j-s_{i-1}
\]

转移代码:

for(int k=i;k<j;k++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
}

而当长度为 \(1\) 的区间由于不需要转移,\(dp_{i,i}\) 本来就是 \(0\)(因为不用合并),所以我们枚举长度 \(l\) 的时候直接从 \(2\) 开始枚举。

注意:由于求的是最小代价,所以要将 \(dp_{i,j}\) 在转移前初始化为极大值。

求 \(dp\) 数组的代码:

for(int l=2;l<=n;l++){//枚举长度
for(int i=1;i<=n-l+1;i++){//枚举起点
int j=i+l-1;//根据起点和长度求出终点
dp[i][j]=1e9;//转移前先初始化成极大值
for(int k=i;k<j;k++){//转移
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);//转移方程如上文所述
}
}
}

求 \(dp\) 数组的代码已经出来了,那么完整代码也一定不难了吧!

2.1.2

代码(已压行):

#include<bits/stdc++.h>
using namespace std;
int s[310],dp[310][310];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
int x;
cin>>x;
s[i]=s[i-1]+x,dp[i][i]=0;
}
for(int l=2;l<=n;l++)for(int i=1;i<=n-l+1;i++){
int j=i+l-1;
dp[i][j]=1e9;
for(int k=i;k<j;k++)dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
}
cout<<dp[1][n]<<endl;
return 0;
}

时间复杂度:\(O(n^3)\)。

2.2 P1435 [IOI 2000] 回文字串

回文词是一种对称的字符串。任意给定一个字符串,通过插入若干字符,都可以变成回文词。此题的任务是,求出将给定字符串变成回文词所需要插入的最少字符数。

比如 \(\verb!Ab3bd!\) 插入 \(2\) 个字符后可以变成回文词 \(\verb!dAb3bAd!\) 或 \(\verb!Adb3bdA!\),但是插入少于 \(2\) 个的字符无法变成回文词。

注意:此问题区分大小写。

记字符串长度为 \(l\),\(1 \le l \le 1000\)。

2.2.1 思路

你没听错,这确实是一道 IOI 的题。

咳咳,不扯那么多,我直接来讲这道题用 区间DP 怎么做。

这个时候,聪明的读者就会说了:诶!这题我会!\(dp_{i,j}\) 表示让 \(s\) 的子串 \(s_i\) 到 \(s_j\) 变成回文字符串所需要最少的插入字符数。而答案,就是 \(dp_{1,n}\) 啦!

那我们的 \(dp_{i,j}\) 应该从哪里转移而来呢?可以看下图理解:

  • 如果 \(s_i=s_j\) 的话,\(dp_{i,j}\) 可以从 \(dp_{i+1,j-1}\) 转移过来。
  • 否则,我们可以从 \(dp_{i+1,j}\) 转移过来并在字符串后面添加一个 \(s_i\) 来保持字符串回文,也可以从 \(dp_{i,j-1}\) 转移过来并在字符串前面添加一个 \(s_j\) 来保持字符串回文。由于我们要求最小长度,应该将前两者取最小值作为答案。

转移代码:

if(s[i]!=s[j]){//如果不相等
dp[i][j]=min(dp[i][j-1],dp[i+1][j])+1;
}
else{//否则直接从dp[i+1][j-1]转移而来
dp[i][j]=dp[i+1][j-1];
}

与上一题一样,长度为 \(1\) 的区间不用转移,因为 \(1\) 个字符组成的字符串一定是回文的字符串。而长度为 \(2\) 的区间可以特殊处理,但是并没有必要,因为只有 \(s_i=s_j\) 的情况才可能越界导致访问到 \(dp_{i,i+1}\) 这样长度为 \(0\) 的区间,但是我们可以将它初始化为 \(0\),这样就不用特殊处理了。

注意:与上题一样,由于求的是最小代价,所以要将 \(dp_{i,j}\) 在转移前初始化为极大值。

2.2.2 代码

代码(已压行):

#include<bits/stdc++.h>
using namespace std;
int dp[1010][1010];
int main(){
string s;
cin>>s;
for(int l=2;l<=s.size();l++)for(int i=0;i<=s.size()-l;i++){
int j=i+l-1;
if(s[i]!=s[j])dp[i][j]=min(dp[i][j-1],dp[i+1][j])+1;
else dp[i][j]=dp[i+1][j-1];
}
cout<<dp[0][s.size()-1]<<endl;
return 0;
}

时间复杂度:\(O(n^2)\)。

3.区间DP 的经典套路题

接下来的题目是 区间DP 的经典套路题,许多 区间DP 题都有这些题的套路。

3.1 P1880 [NOI1995] 石子合并

在一个圆形操场的四周摆放 \(N\) 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 \(2\) 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 \(N\) 堆石子合并成 \(1\) 堆的最小得分和最大得分。

\(N \le 200\)。

3.1.1 思路

这道题和石子合并唯一的区别就是石子是环形排列。那我们应该怎样解决呢?

我们可以考虑一下环形合并带来的影响。

我们先画一个环:

我们考虑可能会跨界(也就包括 \(5\) 的石子堆合并到包括 \(1\) 的石子堆的情况,有以下几种:









这五种合并方式会导致越界。

那我们应该如何解决越界的情况呢?如果我们把一个合并的环环拉成一条线的话:

发现了吗!如果我们把整个 \(1 \thicksim n\) 的数组复制两边,再把区间长度限制到 \(n\) 的话,就能解决环形的问题了:

所以!代码也就很简单了!

3.1.2 代码

代码:

#include <bits/stdc++.h>
using namespace std;
int dp[210][210],dp2[210][210],a[210],s[210];
int main(){
int n,ans1=1e9,ans2=-1e9;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
s[i]=s[i-1]+a[i];//前缀和
a[n+i]=a[i];//求n+1到2n的a数组
}
for(int i=n+1;i<=2*n;i++){//求n+1到2n的前缀和
s[i]=s[i-1]+a[i-n];
}
for(int l=2;l<=n;l++){//长度最大只有n
for (int i=1;i<=2*n-l+1;i++){
int j=i+l-1;
dp[i][j]=1e9;//求最小值
for (int k=i;k<=j-1;k++){//同石子合并弱化版
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
}
dp[i][j]+=s[j]-s[i-1];
}
}
for(int l=2;l<=n;l++){//求最大值
for(int i=1;i<=2*n-l+1;i++){
int j=i+l-1;
for(int k=i;k<=j-1;k++){
dp2[i][j]=max(dp2[i][j],dp2[i][k]+dp2[k+1][j]);
}
dp2[i][j]+=s[j]-s[i-1];
}
}
for(int i=1;i<=n;i++){//找答案
ans1=min(ans1,dp[i][n+i-1]);
ans2=max(ans2,dp2[i][n+i-1]);
}
cout<<ans1<<endl<<ans2<<endl;
return 0;
}

3.2 [NOIP 2003 提高组] 加分二叉树

设一个 \(n\) 个节点的二叉树 \(\text{tree}\) 的中序遍历为\((1,2,3,\ldots,n)\),其中数字 \(1,2,3,\ldots,n\) 为节点编号。每个节点都有一个分数(均为正整数),记第 \(i\) 个节点的分数为 \(d_i\),\(\text{tree}\) 及它的每个子树都有一个加分,任一棵子树 \(\text{subtree}\)(也包含 \(\text{tree}\) 本身)的加分计算方法如下:

\(\text{subtree}\) 的左子树的加分 \(\times\) \(\text{subtree}\) 的右子树的加分 \(+\) \(\text{subtree}\) 的根的分数。

若某个子树为空,规定其加分为 \(1\),叶子的加分就是叶节点本身的分数。不考虑它的空子树。

试求一棵符合中序遍历为 \((1,2,3,\ldots,n)\) 且加分最高的二叉树 \(\text{tree}\)。要求输出

  1. \(\text{tree}\) 的最高加分。

  2. \(\text{tree}\) 的前序遍历。

\(1 \le n < 30\)。

3.2.1 思路

这道题一眼看上去并不是 区间DP,其实不然。

题目要求我们建立一颗中序遍历为 \(1,2,\cdots,n\) 的树,我们可以看看中序遍历为 \(1,2,\cdots,n\) 的树有什么性质。

我们发现,每一个节点的左儿子的编号都会小于本节点的编号,每一个节点的右儿子的编号都会大于本节点的编号。于是,\(dp_{i,j}\) 就能表示 \(i\) 到 \(j\) 的节点按中序遍历的顺序为 \(i,i+1,\cdots,j\) 所得到的树的最大得分。

\(dp_{i,j}\) 怎么转移呢?我们可以看看上面的图片。

我们可以发现,在 \(i,j\) 区间内建成的树,如果让 \(k\) 作为根,那么这两个子树一定就是 \(i,k-1\) 和 \(k+1,j\),得分就是 \(dp_{i,k-1} \times dp_{k+1,j}+d_k\)。我们只要取最大值就行了。

所以转移代码就是:

for(int k=i+1;k<j;k++){
if(dp[i][k-1]*dp[k+1][j]+a[k]>dp[i][j]){
dp[i][j]=dp[i][k-1]*dp[k+1][j]+a[k];
}
}

但是,如果区间长度为 \(2\),此时必然只有一个子树,但题目中说了 若某个子树为空,规定其加分为 1,于是我们就可以加一个 while 特判。

那我们应该如何输出最终的建成的树呢?我们可以建立一个前驱数组 \(fa\),\(fa_{i,j}\) 表示 \(i,j\) 这棵树是的根是 \(fa_{i,j}\),也就是把这棵树分成了 \(i,fa_{i,j}-1\),\(fa_{i,j}+1,j\) 这两个部分,然后递归求解。

题目要求前序输出这棵树,我们就可以先输出这棵树的根 \(fa_{i,j}\),然后在 DFS 递归 \(i,fa_{i,j}-1\) 和 \(fa_{i,j}+1,j\) 这两棵子树,如果 \(i=j\) 就代表递归到叶子结点了,直接输出 \(i\) 即可。

输出代码:

void print(int l,int r){
if(l==r){//如果是叶子节点
cout<<l<<' ';//直接输出
return;
}
cout<<fa[l][r]<<' ';//先输出根节点
if(fa[l][r]!=l)print(l,fa[l][r]-1);//递归左节点
if(fa[l][r]!=r)print(fa[l][r]+1,r);//递归右节点
return;
}

于是!这道题基本上就解决了!

3.2.2 代码

代码(已压行):

#include<bits/stdc++.h>
using namespace std;
int a[50],dp[50][50],fa[50][50];
void print(int l,int r){
if(l==r){
cout<<l<<' ';
return;
}
cout<<fa[l][r]<<' ';
if(fa[l][r]!=l)print(l,fa[l][r]-1);
if(fa[l][r]!=r)print(fa[l][r]+1,r);
return;
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],dp[i][i]=a[i],fa[i][i]=i;
for(int l=2;l<=n;l++)for(int i=1;i<=n-l+1;i++){
int j=i+l-1;
dp[i][j]=max(dp[i+1][j]+a[i],dp[i][j-1]+a[j]);
fa[i][j]=(dp[i+1][j]>dp[i][j-1]?i:j);
for(int k=i+1;k<j;k++)if(dp[i][k-1]*dp[k+1][j]+a[k]>dp[i][j]){
dp[i][j]=dp[i][k-1]*dp[k+1][j]+a[k];
fa[i][j]=k;
}
}
cout<<dp[1][n]<<endl;
print(1,n);
return 0;
}

3.3 关路灯

某一村庄在一条路线上安装了 \(n\) 盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。

为了给村里节省电费,老张记录下了每盏路灯的位置和功率,他每次关灯时也都是尽快地去关,但是老张不知道怎样去关灯才能够最节省电。他每天都是在天亮时首先关掉自己所处位置的路灯,然后可以向左也可以向右去关灯。

现在已知老张走的速度为 \(1m/s\),每个路灯的位置(是一个整数,即距路线起点的距离,单位:\(m\))、功率(\(W\)),老张关灯所用的时间很短而可以忽略不计。

请你为老张编一程序来安排关灯的顺序,使从老张开始关灯时刻算起所有灯消耗电最少(灯关掉后便不再消耗电了)。

\(1 \le n \le 200\)。

3.3.1 思路

我们知道老张关灯的时间不计,那么如果有一连串的灯,他应该全部关掉,如下图:

如果我们只关掉了部分的灯,那么为了关掉剩余的灯,我们就要折返重新关掉一遍,这样就多出来两站灯的功耗,所以这样肯定不是最优的。

我们还可以发现,如果把一条路径上的灯全关掉,关掉会形成一个区间。于是,我们可以用 区间DP 来解决这道题,\(dp_{i,j}\) 表示关掉 \(i,j\) 区间的灯所需要的最小功耗。

那么我们应该怎样计算功耗呢?请看下图:

相信大家应该都懂了吧。那 \(dp_{i,j}\) 应该从哪里转移而来呢?这似乎成了个问题,因为我们不知道关完 \(i,j\) 的灯后我们在哪里。

现在面临着两个选择:

  1. 增加一个维度表示位置,但是前面的推论可能推倒重来
  2. 直接不演了

额...还是选 \(1\) 吧。我们考虑多加一个维度 \(dp_{i,j,0}\) 或 \(dp_{i,j,1}\) 表示关完 \(i,j\) 区间的灯最后停在左边或停在右边的答案。这样转移就好写多了:

我们先考虑 \(dp_{i,j,0}\),也就是关完 \(i,j\) 的灯停在左边的转移。

不难看出,\(dp_{i,j,0}\) 是由 \(dp_{i+1,j,0}\) 从 \(i+1\) 走到 \(i\) 来的,也可以从 \(dp_{i+1,j,1}\) 从 \(j\) 走到 \(i\) 来的。那么这时所消耗的功耗就是 \(a_1 + a_2 + \cdots + a_i\) 和 \(a_{j+1} + \cdots + a_n\) 相加在乘上时间(也就是距离,用 \(c\) 数组表示) \(c_{i+1} - c_i\),如果我们用前缀和记录就是 \((s_n - s_j + s_i) \times (c_j - c_i)\)。

\(dp_{i,j,1}\) 同理,这边也就不过多赘述了。而然答案的话也很好猜,就是 \(\min(dp_{1,n,0},dp_{1,n,1})\)。

如果你看懂了,写出代码应该就不难了吧

3.3.2 代码

代码(已压行):

#include<bits/stdc++.h>
using namespace std;
int a[110],b[110],s[110],dp[110][110][5];
int main(){
int n,t,sum=0;
cin>>n>>t;
for(int i=1;i<=n;i++)cin>>a[i]>>b[i],s[i]=s[i-1]+b[i];
memset(dp,0x3f,sizeof(dp));
dp[t][t][0]=dp[t][t][1]=0;
for(int l=2;l<=n;l++)for(int i=1;i<=n;i++){
int j=i+l-1;
dp[i][j][0]=min(dp[i+1][j][0]+(s[n]-s[j]+s[i])*(a[i+1]-a[i]),dp[i+1][j][1]+(s[n]-s[j]+s[i])*(a[j]-a[i]));
dp[i][j][1]=min(dp[i][j-1][1]+(s[n]-s[j-1]+s[i-1])*(a[j]-a[j-1]),dp[i][j-1][0]+(s[n]-s[j-1]+s[i-1])*(a[j]-a[i]));
}
cout<<min(dp[1][n][0],dp[1][n][1])<<endl;
return 0;
}

4.作业

  1. T1:P2858 [USACO06FEB] Treats for the Cows G/S,难度:\(2/5\)。
  2. T2:P9325 [CCC 2023 S2] Symmetric Mountains,难度:\(2.5/5\)。
  3. P3146 [USACO16OPEN] 248 G,难度:\(3/5\)。
  4. P1063 [NOIP 2006 提高组] 能量项链,难度:\(3.5/5\)。
  5. P4805 [CCC 2016] 合并饭团,难度:\(4/5\)。
  6. P4302 [SCOI2003] 字符串折叠,难度:\(4.5/5\)。

5.闲话

希望大家能喜欢我的文章,点个赞吧 QwQ

蒟蒻不才,膜拜大佬。如果文章有错字等问题,请在评论区@我。

区间DP详解的更多相关文章

  1. 数位DP 详解

    序 天堂在左,战士向右 引言 数位DP在竞赛中的出现几率极低,但是如果不会数位DP,一旦考到就只能暴力骗分. 以下是数位DP详解,涉及到的例题有: [HDU2089]不要62 [HDU3652]B-n ...

  2. 动态规划晋级——HDU 3555 Bomb【数位DP详解】

    转载请注明出处:http://blog.csdn.net/a1dark 分析:初学数位DP完全搞不懂.很多时候都是自己花大量时间去找规律.记得上次网络赛有道数位DP.硬是找规律给A了.那时候完全不知数 ...

  3. 数位DP详解

    算法使用范围 在一个区间里面求有多少个满足题目所给的约束条件的数,约束条件必须与数自身的属性有关 下面用kuangbin数位dp的题来介绍 例题  不要62 题意:在一个区间里面求出有多少个不含4和6 ...

  4. 数位dp详解&&LG P2602 [ZJOI2010]数字计数

    数位dp,适用于解决一类求x~y之间有多少个符合要求的数或者其他. 例题 题目描述 杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除 ...

  5. 状压DP详解(位运算)

    前言: 状压DP是一种非常暴力的做法(有一些可以排除某些状态的除外),例如dp[S][v]中,S可以代表已经访问过的顶点的集合,v可以代表当前所在的顶点为v.S代表的就是一种状态(二进制表示),比如 ...

  6. 状态压缩dp 状压dp 详解

    说到状压dp,一般和二进制少不了关系(还常和博弈论结合起来考,这个坑我挖了还没填qwq),二进制是个好东西啊,所以二进制的各种运算是前置知识,不了解的话走下面链接进百度百科 https://baike ...

  7. 线性DP详解

    顾名思义,线性DP就是在一条线上进行DP,这里举一些典型的例子. LIS问题(最长上升子序列问题) 题目 给定一个长度为N的序列A,求最长的数值单调递增的子序列的长度. 上升子序列B可表示为B={Ak ...

  8. 状态压缩动态规划(状压DP)详解

    0 引子 不要999,也不要888,只要288,只要288,状压DP带回家.你买不了上当,买不了欺骗.它可以当搜索,也可以卡常数,还可以装B,方式多样,随心搭配,自由多变,一定符合你的口味! 在计算机 ...

  9. 状压DP详解+题目

    介绍 状压dp其实就是将状态压缩成2进制来保存 其特征就是看起来有点像搜索,每个格子的状态只有1或0 ,是另一类非常典型的动态规划 举个例子:有一个大小为n*n的农田,我们可以在任意处种田,现在来描述 ...

  10. 树形DP详解+题目

    关于树形dp 我觉得他和线性dp差不多 总结 最近写了好多树形dp+树形结构的题目,这些题目变化多样能与多种算法结合,但还是有好多规律可以找的. 先说总的规律吧! 一般来说树形dp在设状态转移方程时都 ...

随机推荐

  1. 【软件】Rhythmbox播放器调节音量

    Rhythmbox播放器调节音量 零.起因 最近换了Ubuntu系统,在写代码时想听歌,故使用Rhythmbox播放器播放一些mp3文件,但同时又要看教程,希望音乐声音小一点,但是找来找去都没有发现R ...

  2. 学习FASTAPI

    弯弯曲曲的学习之路 学了那么多语言,都是半途而废.包括java,Javascript,go,php,ruby等等乱七八糟. 除了c的自由和pascal的优美,FoxPro简单,其他都似懂非懂入不了法眼 ...

  3. 正反代理-nginx安装

    参考文章:https://www.cnblogs.com/ysocean/p/9384877.html 先预祝一下成功 废话不多说,开始吧,步骤不多 下载地址 https://nginx.org/en ...

  4. spring cloud alibaba的小坑:Caused by: com.alibaba.nacos.api.exception.NacosException: endpoint is blank报错问题,

    一.是因为加添spring cloud alibaba配置中心依赖和bootstrap.xml又不使用的问题

  5. oracle的order by 中文排序原理

    近期发现oracle的order by中文排序并不是完全按照拼音排序的 经过测试发现oracle的order by中文排序是按照中文的ASCII码排序的 查询字符ASCII码 select ascii ...

  6. Redis实现高并发场景下的计数器设计

    大部分互联网公司都需要处理计数器场景,例如风控系统的请求频控.内容平台的播放量统计.电商系统的库存扣减等. 传统方案一般会直接使用RedisUtil.incr(key),这是最简单的方式,但这种方式在 ...

  7. CSS定位的写法

    如上图,商品添加完成后,需要验证商品是否添加成功,通过验证商品列表内是否存在指定名称的商品即可实现验证 浏览器自动获取的xpath=//*[@id="ProductName-divrid53 ...

  8. pytorch 实战教程之路径聚合网络PANet(Path AggregationNetwork)代码实现 PANet原理详解

    原文作者:aircraft 原文链接:pytorch 实战教程之路径聚合网络PANet(Path AggregationNetwork)代码实现 PANet原理详解        学习YOLOv5前的 ...

  9. 你了解 Java 的类加载器吗?

    Java 类加载器(ClassLoader) Java 中的类加载器是用于加载 .class 文件到 JVM 中的组件,它的核心作用是将字节码(.class 文件)加载到内存,并且使它能够被 JVM ...

  10. symfony或doctrine报错:Object of class App\Entity\* could not be converted to string

    报错: Catchable Fatal Error: Object of class App\Entity\ProjectType could not be converted to string 版 ...