算法部分 基础3

一、动态规划的简述

  递归到动规的一般转化方法:递归函数有 n 个参数,就定义了一个 n 维数组,数组的下标是递归函数参数的取值范围,数组元素的值是递归函数的返回值,这样就可以从边界值开始,逐步填充数组,相当于计算递归函数值的逆过程。
  动态规划解题的一般思路:

1. 将原问题分解为子问题。把原问题分解为若干子问题,子问题和原问题形式相同
或类似,只不过规模变小了。子问题都解决,原问题就解决了。并且子问题的解一旦
被求出就会被保存,所以每个子问题只需要求解一次。
2. 确定状态。在用动态规划解题时,将子问题相关的各个变量的一组取值,称为“
状态”。一个“状态”对应于一个或者多个子问题,所谓某个“状态”下的值,就是这个
“状态”下所对应的子问题的解。所有“状态”的集合,构成问题的“状态空间”,“状态空间”的大小,与动态规划
解决问题的时间复杂度直接相关。整个问题的时间复杂度就是状态数目乘以计算每个
状态所需要的时间。
3. 确定一些初始状态(边界状态)的值。
4. 确定状态转移方程。找出不同的状态之间如何迁移--即如何从一个或多个“值“已
知的”状态“,求出另一个”状态“的”值“。状态的迁移可以用递推公式表示,这个递推
公式就叫做”状态转移方程“。

  能用动态规划解决的问题的特点

1. 问题具有最优子结构的性质。 如果问题的最优解所包含的子问题的解也是最优的,
就称问题具有最优子结构性质。
2. 无后效性。 当前的若干个状态值一旦确定,则此后过程的演变就只和这若干状态的
值有关,和之前是采取哪种手段或者经过哪条路径演变到当前的这若干个状态,没有关系。

二、动态规划的例子

1. 数字三角形1

  问题简述:

        73   88   1   02   7   4   4
4   5   2   6   5
上面的三角形中寻找一条从顶部到底边的路径,使得路径上所需要经过的数字之和最大。
路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路
径。

  具体形式如下,

输入:
5 // 三角形行数。下面是三角形
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
要求求出最大和

  解题思路:

用二维数组存放三角形。D(r, j): 第 r 行第 j 列数字(r, j从 1 开始算)
maxSum(r, j): 从 D(r, j) 到底边的各条路径中,最佳路径的数字之和。
问题:求 maxSum(1, 1),从 (1, 1) 开始走,得到最大的值。典型的一个递归问题:
从 D(r, j) 出发,下一步只能走 D(r+1, j)或者D(r+1, j+1). 故对于 N 行的三
角形:
if( r == N ) maxSum(r, j) = D(r, j)
else maxSum(r, j) = max{maxSum(r+1, j), maxSum(r+1, j+1)} + D(r, j)
可以理解作:
1. 当前下,未来值的最大 + 当前值 为现在的最大值。
2. 最后终止值为 D(r, j) , 因为只有当前值了。

  程序如下,

int D[MAX][MAX];
int n;
int maxSum(int i, int j){// 这里返回的是 (i + 1, j) 或 (i + 1, j + 1)if(i == n) return D[i][j]; int x = maxSum(i + 1, j);int y = maxSum(i + 1, j + 1);// D[i][j] 是当前层的,max(x, y) 是未来最大的return max(x, y) + D[i][j];
}int main(){int i, j;cin >> n;for(int i = 1; i <= n; i++){for(int j = 1; j <= i; j++){cin >> D[i][j];}}cout << maxSum(1, 1) << endl;
}

  但是这个程序的时间复杂度特别大,如果数据比较大,需要算很久很久。因为这里面有很多的重复计算,计算过的依旧要再次遍历。

上面程序有很多重复计算:7_13_1   8_18_1   1_2   0_12_1   7_3   4_3   4_1
4_1   5_4   2_6   6_4   5_1
下标是重复调用 maxSum 次数。
如果采用递归的方法,深度遍历每条路径,存在大量重复计算。则时间复杂度是 2^n,
对于 n = 100, 是肯定超时的。因此需要计算时候,改进方法为:
每计算出一个 maxSum(i, j) 保存起来,下次用时候直接调用就可以,则避免很多
重复计算。那么用 O(n^2) 时间完成计算, 相当于直接读取一下就行了。因此三角
形的数字总数为 n(n+1)/2.

  这个就是动态规划的一般思路,把重复计算的部分用数组存起来,所以改进程序如下,

#include <iostream>
#include <algorithm>
#define MAX 20using namespace std;int D[MAX][MAX];
int n;
int sum_Max[MAX][MAX];int maxSum(int i, int j){// 用最后的当前值填充,对应的是 (i + 1, j) 或 (i + 1, j + 1)// 作为终止条件,供下面的语句调用,实现上面未优化程序中的作用if(i == n) sum_Max[i][j] = D[i][j];// 没有用过的用 -1 填充,计算得到的不为 -1,直接取就行if(sum_Max[i][j] != -1) return sum_Max[i][j];else{int x = maxSum(i + 1, j);int y = maxSum(i + 1, j + 1);sum_Max[i][j] = max(x, y) + D[i][j];}// 这里最后一次调用只会返回倒数第二层 sum_Maxreturn sum_Max[i][j];
}int main(){int i, j;cin >> n;for(int i = 1; i <= n; i++){for(int j = 1; j <= i; j++){cin >> D[i][j];sum_Max[i][j] = -1;}}cout << maxSum(1, 1) << endl;
}

运行结果如下,

  递归的难点在于以下几个部分,

1.  if(i == n) sum_Max[i][j] = D[i][j];
这里是用最后的当前值填充,对应的是 (i + 1, j) 或 (i + 1, j + 1)
还有 作为终止条件,供 2 语句调用,实现上面未优化程序中的作用
2. if(sum_Max[i][j] != -1) return sum_Max[i][j];
else{}
这里初始时候用 -1 填充,计算得到的不为 -1,直接取就行,数组作为记录
3. return sum_Max[i][j];
这里最后一次调用只会返回倒数第二层 sum_Max

  还需要讨论的一个点是,

老师给的程序:
if(sum_Max[i][j] != -1) return sum_Max[i][j];if(i == n) sum_Max[i][j] = D[i][j];
else{int x = maxSum(i + 1, j);int y = maxSum(i + 1, j + 1);sum_Max[i][j] = max(x, y) + D[i][j];
}
这里应该返回是靠编译器来执行,如果在最后一层要先检测
if(sum_Max[i][j] != -1) return sum_Max[i][j];
然后把 sum_Max[i][j] 赋值给 D[i][j]
if(i == n) sum_Max[i][j] = D[i][j];
如果要有返回值要运行到这一步
return sum_Max[i][j];我本来以为老师的好像有问题,其实我没考虑到最后 return sum_Max[i][j]; 这一
步,在写最后总结的时候才发现,我这样写复杂度会增加一点点,在最后 i = n 时候,
其实也可以忽略不记,所以进行了记录。
自己改写的
if(i == n) sum_Max[i][j] = D[i][j];if(sum_Max[i][j] != -1) return sum_Max[i][j];
else{int x = maxSum(i + 1, j);int y = maxSum(i + 1, j + 1);sum_Max[i][j] = max(x, y) + D[i][j];
}两种方式都可以运行得到正确的结果。

  为了再帮助理解,这里把递归变为递推来进行理解,如下面这种方式

利用递推式:sum_Max[i][j] = max(x, y) + D[i][j];
并且一开始:sum_Max[4][0:4] = {4, 5, 2, 6, 5};
D(i, j) 7                maxSum   303 8                       23 21         8 1 0                     20 13 10     2 7 4 4                   7  12 10 10       4 5 2 6 5                 4  5  2  6  5

  因此,递归程序转化为递归程序优化为,

#include <iostream>
#include <algorithm>
#define MAX 20using namespace std;int D[MAX][MAX];
int n;
int sum_Max[MAX][MAX];int main(){int i, j;cin >> n;for(int i = 1; i <= n; i++){for(int j = 1; j <= i; j++){cin >> D[i][j];}}for(int i = 1; i <= n; i++){sum_Max[n][i] = D[n][i];}for(int i = n - 1; i >= 1; i--){for(int j = 1; j <= i; j++){sum_Max[i][j] = max(sum_Max[i + 1][j], sum_Max[i + 1][j + 1]) + D[i][j];}}cout << sum_Max[1][1] << endl;
}

  最后还有一个空间优化的方法,可以考虑用

1. 用一维数组存放 sum_Max ,相当于最下面的计算之后就不会用了,因此覆盖掉就可以;
2. 也可以用 D 的第 n 行代替 maxSum;上面的的思路挺好,有一定用处,这里程序就不写上去了。

2. 最长上升子序列

  问题简述:从名字就可以理解,举个例子,

对于序列
(1, 7, 3, 5, 9, 4, 8)
有一些上升子序列
(1, 7), (3, 4, 8) 等等
这些子序列最长是 4, 为 (1, 3, 5, 8)

  要求如下,

输入数据:
输入的第一行是序列的长度 N (1 <= N <= 1000). 第二行给出序列中的 N 个整数,
这些整数的取值范围都在 0 到 10000.
输出要求:
最长上升子序列的长度例如,
输入:
7
1 7 3 5 9 4 8
输出:
4

   这里一定要注意,动态规划解决方案和分析过程下面诠释的很完善,一定认真的看完。 分析如下,

找子问题”求序列的前 n 个元素的最长上升子序列的长度“ 是个子问题,但这样分解,不具
有”无后效性“。假设 F(n) = x, 但可能有多个序列不满足 F(n) = x. 有的序列的最后一个元
素比 a_{n+1} 下,则加上 a_{n+1} 就能形成更长上升子序列; 有的序列最后一个元
素不必 a_{n+1} 小 ...... 以后的事情如何达到状态 n 的影响,不符合 "无后效
性"。上面不满足要求,换个子问题的找法,
1. 找子问题:”求以 a_k(k = 1, 2, ... N) 为终点的最长上升子序列的长度“。一个上升子
序列中最右边的那个数,称为该子序列的 "终点"。虽然这个子问题和原问题形式上并不完全一样,但是只要这 N 个子问题都解决了,
那么这 N 个子问题的解中,最大的那个就是整个问题的解。2. 确定状态:子问题只有一个变量 ---- 数字的位置相关。因此序列中数的位置 k 就是 "状
态", 而状态 k 对应的 "值", 就是以 a_k 作为 ”终点“ 的最长上升子序列的长度。
状态一共有 N 个。3. 找出状态转移方程:maxLen(k) 表示以 a_k 作为 ”终点“ 的最长上升子序列的长度,那么:初始状态:maxLen(1) = 1maxLen(k) = max{ maxLen(i): 1 <= i < k 且 a_i < a_k 且 k 不等于 1} + 1, 如果找不到这样 i , 那么 maxLen(k) = 1maxLen(k) 的值,就是在 a_k 左边,”终点“ 数值小于 a_k , 且长度最大的那
个上升子序列的长度再加 1 . 因为 a_k 左边任何 ”终点“ 小于 a_k 的子序列,加上
a_k 后就能形成一个更长的上升子序列。

   因此,程序如下,

#include <iostream>
#include <cstring>
#include <algorithm>using namespace std;const int MAXN = 1010;
int a[MAXN];
int maxLen[MAXN];int main(){int N;cin >> N;for(int i = 1; i <= N; i++){cin >> a[i]; maxLen[i] = 1;}for(int i = 2; i <= N; i++){// 每次求以第 i 个数为终点的最长上升子序列的长度for(int j = 1; j < i; j++){// 查看以第 j 个数为终点的最长上升子序列if(a[i] > a[j])// 找出第 i 和 前面 + 1 的长度作比较// 因为 a_k 左边任何 ”终点“ 小于 a_k 的子序列,// 加上a_k 后就能形成一个更长的上升子序列。maxLen[i] = max(maxLen[i], maxLen[j] + 1);}}// 在某个区间输出最大值 [1, N+1) 的值cout << *max_element(maxLen + 1, maxLen + N + 1); // 在某个区间输出最大值
} // 时间复杂度为 O(n^2)

   运行结果如下,

   程序难点是这个:

1. if(a[i] > a[j])maxLen[i] = max(maxLen[i], maxLen[j] + 1);
只要 a[i] > a[j] 了后面的序列都要 + 1, 所以 maxLen[j] + 1
并且是用 maxLen[i] 记录从 j 一直到 i 变化的情况,通过 max 实现2. *max_element(a, a+N)
其中参数输入的是地址范围,并且区间是左闭右开,max_element 返回是迭代器
* max_element 的是值

   理解重要的方法,动态规划主要想两点:1. 想第一到二步如何执行,想最后一步如何执行;2. 动态规划数组只保存当前最优的方案,不优的方案会被覆盖点。那么就清晰了。

3. 最长公共子序列( 字符串类型 )

  对动态规划的总结,

1. 递归型
优点:直观,容易编写;
缺点:可能导致递归层数太深导致爆栈(可能相对小些),函数调用会带来额外的开销。无
法用滚动数组节省空间。总的来说,比递推型慢。2. 递推型
优点:效率高,有可能使用滚动数组节省空间;

  问题描述,给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。

举个例子,
输入:
abcfbc abfcab
programming contest
abcd mnp
输出:
4
2
0

  分析

输入两个串 s1, s2:设 maxLen(i, j) 表示:s1 的左边 i 个字符串形成的子串,与 s2 左边的 j 个字串形成的字串的最长公共子序列的长度 (i, j 从 0 开始算)maxLen(i, j) 就是 ”状态“假定 len1 = strlen(s1), len2 = strlen(s2)
那么题目就是要求 maxLen(len1, len2)边界条件:
maxLen(n, 0) = 0, n = 0, 1, ... len1
maxLen(0, n) = 0, n = 0, 1, ... len2递推公式:
if( s1[i - 1] == s2[j - 1] ) // s1 的最左边字符就是 s1[0]maxLen(i, j) = maxLen(i - 1, j - 1) + 1;
elsemaxLen(i, j) = max(maxLen(i, j - 1), manLen(i - 1, j));
因此时间复杂度为 O(m, n) m, n 两个字符串长度

  为什么 else 式子成立呢,看下面这张图

  分析

本题的特殊性,对上面的分析如下分析就分成两步:
1. s1[i-1] != s2[j-1], maxLen(s1, s2) 不会比 maxLen(s1, s2_{j-1})
和 maxLen(s1_{i-1}, s2_{j})中任何一个小,通过正常思考就可以得到;
2. 接着证明 s1[i-1] != s2[j-1], maxLen(s1, s2) 不会比 maxLen(s1, s2_{j-1})
和 maxLen(s1_{i-1}, s2_{j}) 中任何一个大第二个使用反证法,
假设 maxLen(s1, s2) 比 两个都大,如果不满足条件,那么则成立。
maxLen(s1, s2) 比 maxLen(s1, s2_{j-1}) 多了一个 s2_{j-1} 字符
或者 maxLen(s1, s2) 比 maxLen(s1_{j-1}, s2) 多了一个 s1_{j-1} 字符
那么必然要满足 s1[i-1] = s2[j-1] ,和条件不成立

  所以程序如下,

#include <iostream>
#include <cstring>using namespace std;char sz1[1000];
char sz2[1000];
int maxLen[1000][1000];
int main(){while(cin >> sz1 >> sz2){int length1 = strlen(sz1);int length2 = strlen(sz2);// 初始条件for(int i = 0; i <= length1; i++){maxLen[i][0] = 0;}for(int j = 0; j <= length2; j++){maxLen[0][j] = 0;}// 状态变化for(int i = 1; i <= length1; i++){for(int j = 1; j <= length2; j++){if(sz1[i-1] == sz2[j-1])// 斜对角的情况maxLen[i][j] = maxLen[i-1][j-1] + 1;else// 不是斜对角的情况maxLen[i][j] = max(maxLen[i][j-1], maxLen[i-1][j]);}}cout << maxLen[length1][length2] << endl;}return 0;
}

运行结果如下,

接着进行理解分析,

其实这里理解的关键就是这个 maxLen 矩阵代表的意义:
开始情况:
0  0  0  0  0  0
0
0
0
0
0
递推中间情况:
0  0  0  0  0  0
0
0     a  b
0     c  d
0
这里,如果满足 if(sz1[i-1] == sz2[j-1]) ,那么 d = a + 1
否则,那就满足 d = max(b, c)因此可以理解为,
maxLen 的 i 行存储的是 s1 的第 i 个字符之前和 s2 所有 j 个字符之前进行对
比,把最大存起来。

4. 最佳加法表达式

  问题描述:有一个由于 1…9 组成的数字串。问如果将 m 个加号放到这个数字串中,在各种可能形成的表达式中,值最小的那个表达式的值是多少。
  解题思路:假定数字串长度是 n,添加完加号后,表达式的最后一个加号添加在第 i 个数字后面,那么整个表达式的最小值,就等于在前 i 个数字中插入 m - 1 个加号所能形成的最小值,加上第 i + 1 到第 n 个数字所组成的数的值(i 从 1 开始算)。具体思路如下,

假设 V(m, n)表示在 n 个数字中插入 m 个加号所能形成的表达式最小值,那么:if m = 0V(m, n) = n 个数字构成的整数
else if n < m + 1V(m, n) = 给定一个很大的数字 Inf
elseV(m, n) = min{V(m-1, i) + Num(i + 1, n)}(i = m...n-1)
Num(i, j) 表示从第 i 个数字到第 j 个数字所组成的数。数字编号从 1 开始计算。这个操作复杂度是 O(j - i + 1), 可以存起来
总的时间复杂度是:O(mn^2)若 n 比较大, long long 不够存放运算过程中的整数,则需要使用高精度计算(用
数组存放大整数,模拟列竖式做加法),复杂度为 O(mn^2)

程序如下,

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>using namespace std;int number[100 + 1];  //存放输入的数字
long long num[100 + 1][100 + 1];
long long dp[100 + 1][100 + 1];
int m, n;// 这两个程序主要作用是初始化
// 数据用数组方式存储,对 num 进行初始化
// 最后保存的数据是以 i 位置之后整数的和
int Num(int i, int j){int k, temp, sum = 0;double t = j - i;for(k = i; k <= j; ++k, --t){temp = pow(10, t);sum = sum + number[k] * temp;}return sum;
}
// 右上三角方式存放数据
void calculate(void){int i, j;for(i = 1; i <= n; ++i)for(j = i;j <= n; ++j)num[i][j] = Num(i, j);
}int main(){cin >> n >> m;              //在 n 个数字中插入 m 个加号int i, j, p, minn = INT_MAX;for(i = 1; i <= n; ++i)           //从下标为 1 开始存放cin >> number[i];calculate();for(i = 1; i <= n; ++i){         //在 i 个数字中插入 j 个加号for(j = 0;j <= m; ++j){if(j == 0)// 原有的 n dp[i][j] = num[1][i];else if(i <= j)dp[i][j] = INT_MAX; else{minn = INT_MAX;for(p = j; p <= i - 1; ++p){if( minn >( dp[p][j-1] + num[p+1][i]))minn = dp[p][j-1] + num[p+1][i];}dp[i][j] = minn;}}}cout << dp[n][m] << endl;return 0;
}

运行结果如下,

分析

int Num(int i, int j){int k, temp, sum = 0;double t = j - i;for(k = i; k <= j; ++k, --t){temp = pow(10, t);sum = sum + number[k] * temp;}return sum;
}
// 右上三角方式存放数据
void calculate(void){int i, j;for(i = 1; i <= n; ++i)for(j = i;j <= n; ++j)num[i][j] = Num(i, j);
}
这两段程序主要是对 num 矩阵进行初始化
举个例子比较好理解,对于输入的
1 8 9 5 4
num 初始矩阵为
1  18  189  1895  189548   89   895   89549    95    954 5     544对于动态规划部分:
minn = INT_MAX;
for(p = j; p <= i - 1; ++p){if( minn >( dp[p][j-1] + num[p+1][i]))minn = dp[p][j-1] + num[p+1][i];}
dp[i][j] = minn;
这里主要实现
在有 m 个符号下怎么插入实现最小值, 用 j = 1:m 来实现递推。

三、总结

  动态规划类型题比较灵活,需要多看一些类型的题,然后总结出这类题的实现方案。其实主要在于子问题的寻找,然后难点就是动态方程的设计,这里需要仔细思考对于子问题如何动态变化寻找最优。

算法基础部分3-动态规划相关推荐

  1. 算法基础:动态规划数组中滚动数组的使用

    这篇文章继续在前一篇文章的基础上介绍动态规划数组的优化方式.很多基础算法本来都是写给我家的小少年看的,结果发现后浪学习的速度远远超出我的想象,在一个周末用这篇文章来纪念一下吧. 目录 斐波那契数列 d ...

  2. 算法基础知识——动态规划

    算法基础知识--动态规划 目录: 基础知识 分治法和动态规划的区别 动态规划算法设计步骤 最优子结构性质定义 动态规划两种等价的实现方法(自顶向下带备忘.自底向上) 子问题图 经典问题 钢条切割 矩阵 ...

  3. 送书 | 你一定能看懂的算法基础书(代码示例基于Python)

    本文引自图灵教育<算法图解> 你一定能看懂的算法基础书:代码示例基于Python:400多个示意图,生动介绍算法执行过程:展示不同算法在性能方面的优缺点:教会你用常见算法解决每天面临的实际 ...

  4. 五大常用算法之二:动态规划算法

    基本概念 动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移.一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划. 基本思想与策略 基本思想与分 ...

  5. java遍历字符串_Java后端开发算法基础面试题分享,你离大厂也许就差这份面试题

    一.算法基础 1. 重建二叉树 题目: 输入一棵二叉树前序遍历和中序遍历的结果,请重建该二叉树. 注意: 二叉树中每个节点的值都互不相同: 输入的前序遍历和中序遍历一定合法: 演示: 给定: 前序遍历 ...

  6. (转)五大常用算法:分治、动态规划、贪心、回溯和分支界定

    分治算法 一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是"分而治之",就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题-- ...

  7. 任务分配算法c语言程序,程序员算法基础——贪心算法

    原标题:程序员算法基础--贪心算法 前言 贪心是人类自带的能力,贪心算法是在贪心决策上进行统筹规划的统称. 比如一道常见的算法笔试题跳一跳: 有n个盒子排成一行,每个盒子上面有一个数字a[i],表示最 ...

  8. 五大常用算法:分治、动态规划、贪心、回溯和分支界定

    算法系列之十六:使用穷举法解猜结果游戏--http://blog.csdn.net/orbit/article/details/7607685 ---------------例子: 麻将PC上发送操作 ...

  9. 算法基础、算法比赛快速入门(java)

    想用Java快速入门算法?这篇文章你得看! 提示:本文章适合想要入门算法,并且想 "快速" 达到一定成果的同学们阅读~ 文章非常非常非常长(可能是你见过最长的算法基础篇章)!!! ...

  10. 【数据结构与算法基础】最短路径问题

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

最新文章

  1. 一小时Thinkphp后台(2)
  2. 深度学习必备数学知识之线性代数篇(附代码实现)
  3. keepalived介绍
  4. linux文件的权限模式,Linux文件权限和访问模式
  5. oracle导入java包时出错,Oracle导入导出的常见错误
  6. jade软件_TEM衍射斑点标定之DM软件
  7. sublime 3 3083验证码
  8. textarea的maxlength属性兼容解决方案
  9. Spring Web MVC 的工作流程
  10. 渗透测试基础-XSS漏洞简析
  11. Linux黑客基础01篇
  12. 程序员与颈椎病(三):颈椎病终极解决办法
  13. linux定时任务生效_linux ( crontab 定时任务命令)
  14. 以神奇“三”为本的逻辑与指号学----皮尔斯逻辑之三
  15. 2022年9月11日(星期天):(原创)骑行环草海
  16. 云计算概念入门和知识普及(转)
  17. 基于PHP的图书商城系统
  18. 英式音标26字母发音规律
  19. slice扩容机制分析
  20. 64位office无法安装

热门文章

  1. SCI投稿中的简写(ADM,AE,EIC等)与状态解读
  2. Vim 实用技术,第 1 部分: 实用技巧(转)
  3. [大牛翻译系列]Hadoop(4)MapReduce 连接:选择最佳连接策略
  4. HTML 5 学习笔记之 canvas 标签
  5. asp.net中读取数据库中的数据可以使用DataReader和DataSet 2种方式(初学者望大家不要笑我)...
  6. OpenStack安装流程(juno版)- 添加镜像服务(glance)
  7. 你真的会玩SQL吗?透视转换的艺术
  8. docker上安装nginx服务
  9. 『科学计算_理论』矩阵求导
  10. 恢复误删除的域用户及几个查询命令