0%

总忘,每次用到都要搜,记录一下

1
2
3
4
5
6
7
8
9
10
11
from functools import cmp_to_key

score = [97, 80, 90, 63, 72, 84]
idx = range(len(score))

# cmp传入的是类似C语言qsort的比较函数,返回负、零、正来表示大小关系
idx = sorted(idx, key=cmp_to_key(lambda i, j: score[i]-score[j]))
print(idx) # idx的序是按照score[idx[i]]的值排的
sorted_score = [score[i] for i in idx]
assert(sorted_score==sorted(score))
print(sorted_score)

在Windows上通过net use,在linux通过mount可以把网络共享路径挂载到本地的一个盘符或一个目录,在Windows上可以直接判断路径是否存在,而在Linux挂载点总是存在的,即使挂载失败也存在一个空的文件夹,所以不能用路径是否存在来判断,可以在df命令中找挂载点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
import platform
import traceback


def exec(cmd:str):
pip = os.popen(cmd)
r = pip.buffer.read()
try:
return r.decode(encoding='utf8')
except Exception as e:
try:
return r.decode(encoding='gb2312')
except Exception as e:
return r
return r


def is_mounted(path):
if platform.system() == 'Windows':
return os.path.exists(path)
else:
return int(exec(f"df -h|grep {path}|wc -l")) > 0

常见的背包问题根据背包的重数和最终求的值可以分为下面6类:

1重 多重 无穷重
最大价值 0-1背包 多重背包 完全背包
方案数 0-1背包计数 多重背包计数 完全背包计数

0-1背包

以最普通的0-1背包来说,

问题描述:

n种物品,每种1个,一个容量为C的背包,第i种物品的体积是w[i],价值是v[i],问背包能装下物品的最大价值。(也就是总体积不超过C的最大物品价值)

思路:

dp[i][c]表示前i个物品在容量为c的背包时能装下的最大价值,每个物品都可以取或者不取,在面对第i个物品时,如果取那么

1
dp[i][c] = dp[i-1][c-w[i]]+v[i] if c>=w[i]

如果不取那么

1
dp[i][c] = dp[i-1][c]

并且dp[i][c]对于c应该是单调不减的,因为容量变大总价值不可能减少。

所以有

1
dp[i][c] >= dp[i][c-1]

综上,我们有

1
2
3
4
dp[i][c] = max(dp[i-1][c], dp[i][c-1]);
if(c>=w[i]) {
dp[i][c] = max(dp[i][c], dp[i-1][c-w[i]]+v[i]);
}

有时候要求背包必须是满的,则没有上述单调性,或者我们总是不考虑单调性,而在给出最终答案前去遍历dp[n-1]得到最大值,鉴于此我们把上式简化,并加上循环

1
2
3
4
5
6
7
8
for(int i=0;i<n;i++) {
for(int c=0;c<=C;c++) {
dp[i][c] = dp[i-1][c];
if(c>=w[i]) {
dp[i][c] = max(dp[i][c], dp[i-1][c-w[i]]+v[i]);
}
}
}

注意到dp[i][c] = dp[i-1][c]相当于对于每个新的物品我们总是在前一物品算好的dp数组上做修改,那么能不能把第一维去掉直接修改呢,问题就在于dp[i-1][c-w[i]]我们会用到c-w[i]位置,而它可能在第i趟循环时修改了,如果我们再利用这个状态相当于物品i被用了两次,所以我们需要第i-1趟循环时的值,考虑到只会引用到c更小是位置,我们可以把内层循环反向,这样就不会在第i趟循环时修改到c-w[i]位置的值了。

1
2
3
4
5
for(int i=0;i<n;i++) {
for(int c=C;c>=w[i];c--) {
dp[c] = max(dp[c], dp[c-w[i]]+v[i]);
}
}

循环完成后 max(dp)就是背包能取得的最大价值。

0-1背包算法时间复杂度O(n*C),空间复杂度O(C)

多重背包

问题描述:

n种物品,每种m[i]个,一个容量为C的背包,第i种物品的体积是w[i],价值是v[i],问背包能装下物品的最大价值。

思路:

如果把m[i]个相同的物品视作m[i]种物品则问题可以规约为0-1背包,时间复杂度O(∑m[i]*C),那么能不能利用m[i]个物品是相同的特性来优化时间复杂度呢?是可以的,对于第i种物品,我们不把它视作m[i]个相同的物品,而是把他们按二进制的方式组合,分成1个物品组合,2个物品组合,4个物品组合,……,这样m[i]个物品就被组合成了log(m[i])个物品,再规约为0-1背包,显然不影响最终解。算法时间复杂度O(∑log(m[i])*C),组合成的新物品可以边循环边生成因此没有空间开销,空间复杂度还是O(C)

完全背包

问题描述:

n种物品,每种无穷个,一个容量为C的背包,第i种物品的体积是w[i],价值是v[i],问背包能装下物品的最大价值。

思路:

虽然每种物品个数是无穷的,但背包实际的容量的有限的,对于第i种物品最多装下C/v[i]个,我们可以令m[i]=C/v[i]即可把问题规约为多重背包问题。而实际上还有更简单的解法,我们在思考0-1背包空间降维时遇到的问题,“问题就在于dp[i-1][c-w[i]]我们会用到c-w[i]位置,而它可能在第i趟循环时修改了,如果我们再利用这个状态相当于物品i被用了两次”,而这对于完全背包问题来说正是我们需要的,因此只有把0-1背包内层循环的顺序改成正向的就实现了完全背包。

1
2
3
4
5
for(int i=0;i<n;i++) {
for(int c=w[i];c<=C;c++) {
dp[c] = max(dp[c], dp[c-w[i]]+v[i]);
}
}

循环完成后 max(dp)就是背包能取得的最大价值。

完全背包的复杂度和0-1背包相同,算法时间复杂度O(n*C),空间复杂度O(C)

0-1背包计数

问题描述:

n种物品,每种1个,一个容量为C的背包,第i种物品的体积是w[i],问恰好装满背包的方案数。

思路:

对于计数问题通常会忽略价值,或者认为价值等于体积,用dp[i][c]表示前i个物品在容量为c的背包时恰好装满的方案数,则有

1
dp[i][c] = dp[i-1][c] + (dp[i-1][c-w[i]] if c>=w[i] else 0)

即要么不用第i个物品,要么用第i个物品。

同样我们可以用0-1背包类似的思想把空间降成1维,内层循环用倒序。

1
2
3
4
5
6
dp[0] = 1;
for(int i=0;i<n;i++) {
for(int c=C;c>=w[i];c--) {
dp[c] += dp[c-w[i]];
}
}

多重背包计数

对于多重背包,要明确一点,相同种类的背包视为相同背包(例如5个第i种背包,取3个只会形成1种方案,而不是5C3种方案),那么还是可以按二进制的方式组合。

完全背包计数

问题描述:

n种物品,每种无穷个,一个容量为C的背包,第i种物品的体积是w[i],问恰好装满背包的方案数。

思路:

和完全背包类似思路,直接上代码:

1
2
3
4
5
6
dp[0] = 1;
for(int i=0;i<n;i++) {
for(int c=w[i];c<=C;c++) {
dp[c] += dp[c-w[i]];
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import math

def coord_dist(lat1, lnt1, lat2, lnt2):
sin = math.sin
cos = math.cos
acos = math.acos
deg2rad = math.radians

R = 63713930
lat1 = deg2rad(lat1)
lnt1 = deg2rad(lnt1)
lat2 = deg2rad(lat2)
lnt2 = deg2rad(lnt2)
l = R * acos(cos(lnt1)*cos(lnt2)*cos(lat1-lat2)+sin(lnt1)*sin(lnt2))
return l

CRON时间表达式

我们知道在Linux上的任务计划可以通过cron服务管理,cron服务设置任务运行时间有着自定义的时间表达式,形如

1
2
# m h  dom mon dow   command
58 8 * * 1-5 /root/run.sh

这条记录表示在每周一到周五的08:58自动运行/root/run.sh脚本,除去command,我们看到时间表达式有m, h, dom, mon, dow几个部分,分别表示分钟、小时、每月几号、月份、星期几。几号和星期几通常是二选一,指定了一个另一个就会指定成*表示任意,也可以都指定成*,则表示每天。

dow1-5表示周一到周五,也可以用逗号分割列举具体星期几,例如1,3,5表示每周一三五,除了周几,其他时间项也都可以使用此表示法。

除了用-表示范围,还可以用/表示频率例如:

1
2
# m   h   dom mon dow   command
0/5 9-14 * * 1-5 /root/run.sh

表示每周一~周五,小时为9点到14点,从0分开始每5分钟运行一次脚本,这样每天第一次运行的时间是09:00,每天最后一次运行的时间是14:55,中间都是每隔5分钟运行一次。

扩展CRON时间表达式

增加秒

一些程序允许使用扩展的CRON时间表达式,一般会增加的表示

可能通过第一位也可能通过最后一位表示。

1
s  m   h   dom mon dow

1
m   h   dom mon dow  s

具体要阅读文档来查询。

增加随机

还有一种扩展是增加随机性指定,通过以字母H开头表示

例如

1
H(55-59) 8  *  *  1-5  /root/run.sh

表示周一到周五,每天[08:55, 09:00)之间的一个随机时间运行/root/run.sh,这样通常是为了缓解多个任务同时启动的压力。

Python中通过带时区的datetime使用croniter

1
2
3
4
5
6
7
8
9
10
11
12
13
from croniter import croniter
from datetime import datetime
from dateutil import tz

now = datetime.now().replace(tzinfo=tz.gettz()) # 得到当前时间(带时区)
cron = croniter('0/5 9-14 * * 1-5', now) # 初始化cron对象

dt = cron.get_next(datetime) # 取得下次触发时间,实际上做的是迭代器去next()然后返回
print(dt)
dt = cron.get_next(datetime) # 所在第二次调用的时候,迭代器继续next(),这里返回的是第二次触发的时间
print(dt)
dt = cron.get_prev(datetime) # 同理,这里将返回第一次触发的时间,而不是初始化时间之前的上一次触发时间
print(dt)

有时候我们即需要上次触发时间,又需要下次触发时间,要么我们理解了迭代器的方式通过取prev再取next的next,要么我们创建两个cron对象分别取prev和next。

croniter允许通过datetime对象或者timestamp(即time.time()的返回值,float类型)来使用cron对象,但是我们的时间表达式是带有时区概念的,所以cron对象必须明确知道我们的时区,否则将不能正确工作,一种方式是我们使用datetime初始化cron对象,取next或prev的时候也使用datetime类型接收,这时候即使时区不明确也不要紧,但是不能使用timestamp接收next和prev的返回值,否则cron对象会默认使用UTC时区导致返回的float值不正确,如果我们使用带时区的datetime来初始化cron对象则没有这个问题,上面例子就是通过带时区的方式初始化的。

另外,python的cron对象是同时支持秒扩展和随机扩展的,秒放在最后一位,使用随机需要在初始化时指定hash_id,不同的hash_id将影响随机结果,例如我还是上面的例子,但是我需要触发时间不在分钟的整点,而在这一分钟内随机的秒数上,就可以写成下面这样

1
cron = croniter('0/5 9-14 * * 1-5 H(0-59)', now, hash_id="hash_id")

安装环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 安装nvm
export NVM_SOURCE=https://gitlab.com/mirrorx/nvm.git
curl -o- https://gitlab.com/mirrorx/nvm/-/raw/master/install.sh | bash

# 查看可安装的node版本
nvm ls

# 用nvm安装指定版本的node和npm
nvm install v14.20

# 切换node和npm版本
nvm use v14.20

# 更换npm源
npm config set registry https://registry.npmmirror.com

构建项目

1
2
3
4
5
6
7
8
# 安装项目依赖
npm install

# 构建项目(会生成dist目录,需要部署到nginx等web服务器或者通过pm2运行再通过nginx设置反向代理)
npm run build

# 调试运行
npm run dev

一些工具

视需求情况安装

1
2
3
4
5
apt install nodejs-dev node-gyp libssl1.0-dev
npm install -g pm2
npm install -g express
npm install -g express-generator
npm install -g compression

其中pm2可以直接启动js作为服务器运行

使用pm2

1
2
3
4
5
6
7
8
9
10
11
# 启动项目
pm2 start app.js --name app

# 查看通过pm2启动的项目
pm2 ls

# 停止项目
pm2 stop app

# 将项目从pm2的列表移除
pm2 delete app

其中app.js为服务器启动脚本,参考脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// app.js
const express = require('express');
//创建web服务器
const app = express();
//导入gzip包
const compression = require("compression")
//文件操作
const fs = require('fs');
const path = require('path');
const chalk = require('chalk')

//启用gzip中间件,在托管之前
app.use(compression())
//托管静态资源
app.use(express.static(path.resolve(__dirname, './dist')))

app.get('/', function(req, res) {
const html = fs.readFileSync(path.resolve(__dirname, './dist/index.html'), 'utf-8')
res.send(html)
})

//启动web服务器
app.listen(8082, res => {
console.log(chalk.yellow('Start Service On 8082'));
});

LVM是 Logical Volume Manager(逻辑卷管理)的简写,它是Linux环境下对磁盘分区进行管理的一种机制,可以将多个物理硬盘进行重新组织,划分成多个逻辑卷并可以动态改变卷大小(例如增加新的硬盘来扩展已有的逻辑卷)

lvm

LVM常用的命令

功能 PV管理 VG管理 LV管理
scan 扫描 pvscan vgscan lvscan
create 创建 pvcreate vgcreate lvcreate
display 显示 pvdisplay vgdisplay lvdisplay
remove 移除 pvremove vgremove lvremove
extend 扩展(增加) vgextend lvextend
reduce 减少 vgreduce lvreduce

PV对应物理卷

例如 PV1 <=> /dev/sda1

VG是卷组,由PV构成的集合,也是由LV构成的集合

在PV上可以划分LV,即逻辑卷

从新的硬盘创建一套LVM的步骤

创建物理卷

1
pvcreate  <后面可以是/dev/sdb这样的未分区整个盘也可以是/dev/sdb1这样的一个分区>

例如:pvcreate /dev/sda

创建卷组

1
vgcreate 卷组名 物理卷

例如:vgcreate vg_abc /dev/sda

扩展卷组

向卷组中增加物理卷

1
2
vgextend vg_abc /dev/sdb
vgextend vg_abc /dev/sdc

创建逻辑卷

从卷组vg_abc创建一个大小为1G的逻辑卷lv_data

1
lvcreate -L 1G vg_abc -n lv_data

从卷组vg_abc创建一个占卷组大小100%的逻辑卷lv_data

1
lvcreate -l 100%VG vg_abc -n lv_data

格式化逻辑卷

1
mke2fs -t ext4 -L "the ext4 on lvm" /dev/vg_abc/lv_data

删除逻辑卷

删除LV将空间还给VG

1
lvremove /dev/vg_abc/lv_data

设置系统启动时自动挂载逻辑卷

/etc/fstab中添加内容:

1
/dev/vg_abc/lv_data /data ext4 defaults 0 0

扩展使用中的逻辑卷的大小

如果使用新购置硬盘,按照前面的方式 创建物理卷扩展卷组,然后再扩展逻辑卷

增加固定大小

1
lvextend -L +10G /dev/mapper/vg_abc-lv_data

按卷组剩余空间百分比扩展

1
lvextend -l +40%FREE /dev/mapper/vg_abc-lv_data

按卷组的总空间百分比扩展

1
lvextend -l +40%VG /dev/mapper/vg_abc-lv_data

调整文件系统的大小使之等于逻辑卷大小

1
resize2fs /dev/mapper/vg_abc-lv_data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

typedef ll COST_T;
typedef ll CAP_T;
constexpr COST_T COST_INF = 0x3f3f3f3f3f3f3f3fll;
constexpr CAP_T CAP_INF = 0x3f3f3f3f3f3f3f3fll;


class MinCostMaxFlow {
struct Edge{
int from, to;
CAP_T flow, cap;
COST_T cost;
};

int n;
CAP_T maxFlow;
COST_T minCost;

vector<vector<int>> g;
CAP_T* a = NULL;
COST_T* d = NULL;
vector<Edge> edges;

bool* vis = NULL;
int* p = NULL;
public:
MinCostMaxFlow(int n): n(n), g(n, vector<int>()), a(new CAP_T[n]), d(new COST_T[n]), vis(new bool[n]), p(new int[n]) {

}

~MinCostMaxFlow() {
delete [] a;
delete [] d;
delete [] vis;
delete [] p;
}

void add_edge(int from, int to, CAP_T cap, COST_T cost) {
Edge temp1 = { from, to, 0, cap, cost };
Edge temp2 = { to, from, 0, 0, -cost }; //允许反向增广
edges.push_back(temp1);
edges.push_back(temp2);
int len = edges.size();
g[from].push_back(len - 2);
g[to].push_back(len - 1);
}

bool bellmanford(int s, int t) {
fill(d, d+n, COST_INF);
d[s] = 0;
memset(vis, 0, sizeof(*vis)*n);
memset(p, -1, sizeof(*p)*n);
p[s] = -1;
a[s] = CAP_INF;
queue<int> Q;
Q.push(s);
vis[s] = true;
while (!Q.empty()) {
int u = Q.front();
Q.pop();
vis[u] = false;
for (int i = 0; i < g[u].size(); i++) {
Edge& e = edges[g[u][i]];
//进行松弛,寻找最短路径也就是最小费用
if (e.cap > e.flow && d[e.to] > d[u] + e.cost) {
d[e.to] = d[u] + e.cost;
p[e.to] = g[u][i];
a[e.to] = min(a[u], e.cap - e.flow);
if (!vis[e.to]) {
Q.push(e.to);
vis[e.to] = true;
}
}
}
}
if (d[t] == COST_INF) return false;
maxFlow += a[t];
minCost += d[t] * a[t];
for (int i = t; i != s; i = edges[p[i]].from) {
edges[p[i]].flow += a[t];
edges[p[i] ^ 1].flow -= a[t];
}
return true;
}


pair<COST_T, CAP_T> solve(int s, int t) {
memset(a, 0, sizeof(*a)*n);
maxFlow = 0;
minCost = 0;

while (bellmanford(s, t))
continue;
return make_pair(minCost, maxFlow);
}
};

编译时查找动态链接库的顺序

编译时会在以下路径查找:
通过编译选项-L 指定的目录
通过链接选项–rpath=指定的目录
通过环境变量LIBRARY_PATH指定的目录(:分割)
通过环境变量LD_LIBRARY_PATH指定的目录(:分割)
/lib
/usr/lib
/etc/ld.so.cache中缓存的路径,通过/etc/ld.so.conf指定,通过ldconfig命令更新缓存。
至于查找的顺序其实不重要,因为只要找到符号通过链接就可以了,而运行时又会按照运行时查找的顺序进行查找,甚至运行时都不在相同环境,链接到的动态库也可能和编译时不是同一个。

指定编译时的动态链接库路径

通过gcc/g++-L编译选项
通过-Wl,--rpath=传递链接选项
设置LIBRARY_PATHLD_LIBRARY_PATH环境变量
CMakeLists.txt中使用link_directories(${LIB_DIRS})来指定,LIB_DIRS是用一个或多个空白字符分割的多个路径
CMakeLists.txt中使用指定rpath,方式如下

1
2
3
4
target_link_libraries(${PROJECT_NAME}
${LIB_LIST}
-Wl,-rpath,'$ORIGIN'/libs,-rpath,'$ORIGIN'/../libs
)

其中'$ORIGIN'是一个特殊的路径,实际上这里是在提前指定运行时的查找路径,'$ORIGIN'表示可执行文件所在目录,另外要注意的是如果指定多个rpath这里不能通过:分割,而是使用多次-rpath选项.
BTW: 我并不清楚为什么要使用'$ORIGIN'而不是.,因为我测试下来效果是一样的。

运行时查找动态链接库的顺序

通过rpath连接选项指定的目录
通过环境变量LD_RUN_PATH指定的目录(:分割)
通过环境变量LD_LIBRARY_PATH指定的目录(:分割)
编译时实际的链接路径(不是查找过的路径,而是找到了这个so的路径)
/lib
/usr/lib

另外可以通过ldd <可执行程序>来查看运行将使用链接库,也可以看到缺少的库。
还可以通过chrpath -l <可执行程序>查看程序被设置的rpath。

指定运行时的动态链接库路径

设置环境变量LD_RUN_PATH(:分割)
设置环境变量LD_LIBRARY_PATH(:分割)
还可以通过chrpath -r <库路径> <可执行程序>修改程序的rpath,但实际上rpath被写在可执行文件中并没有为修改它预留空间,所以只能修改成相同长度或更短的路径,所以显然这不是一个常规操作。
另外rpath只影响这个可执行程序本身而不能传递给其加载的so,如果我从程序a加载了动态库b,而动态b需要加载动态库c,即使我把b、c放在相同路径,a通过rpath的方式找到了b,而b还是找不到c,因为b的rpath里并没有这个路径。
所以总得来说最靠谱的方式还是指定LD_LIBRARY_PATH,在linux上可以通过下面的方式在启动程序同时设置环境变量

1
LD_LIBRARY_PATH=<动态库目录>:$LD_LIBRARY_PATH <可执行文件>

设置LD_LIBRARY_PATH同时保留了之前的值放在后面,这样设置的环境变量只影响可执行文件启动的程序,包括间接加载的动态链接库,

某些场景下我们需要在Docker中运行Docker,例如在Docker中部署Jenkins,在Jenkins的任务中对项目进行打包测试发布,而项目也是基于Docker的。

首先明确我们需要的是什么,Docker其实包含服务器(Docker Engine或者叫Docker Daemon)和客户端(Docker CLI),我们需要的是在容器内能够使用Docker客户端访问到Docker服务器,而Docker服务器其实不必运行在容器内,可以运行在宿主机上。

docker的客户端和服务器有一个参数决定了两者的通讯方式,默认情况下使用/var/run/docker.sock进行通讯,这个文件表示一个Unix Socket,Unix Socket是一种类似我们熟知的TCP/UDP的进程间通讯方式,我们也可以通过增加-H tcp://0.0.0.0:2376参数在启动时修改默认的通讯方式为TCP2376端口通讯。

1
-H, --host=[unix:///var/run/docker.sock]: tcp://[host]:[port][path]

方法一

我们可以通过在创建容器时指定-v /var/run/docker.sock:/var/run/docker.sock把宿主机的Socket映射到容器内,容器内的Docket客户端就相当于直接连到了宿主机的Docker Engine。

这样容器内和容器外使用的是同一个engine可以相互操作对方创建的镜像有一定风险。

方法二

那么还有一个方法就是使用别人制作的特殊系统镜像,镜像名称通常包含dind字样,表示支持Docker in Docker,例如gitlab/dind,这种镜像启动时需要--privileged参数,启动后容器内部预装了docker环境,容器内外的engine是独立不互通的,这种方法的问题是系统的版本是固定的,取决于dind镜像提供的什么版本。

方法三

替换docker运行时为sysbox

1
docker run --runtime=sysbox-runc --name sysbox-dind -d docker:dind

需要安装sysbox,参考sysbox/install-package.md at master · nestybox/sysbox (github.com)

因为安装前需要停止并删除所有容器,我并没有尝试过。