Python自定义排序比较方法
总忘,每次用到都要搜,记录一下
1 | from functools import cmp_to_key |
总忘,每次用到都要搜,记录一下
1 | from functools import cmp_to_key |
在Windows上通过net use
,在linux通过mount
可以把网络共享路径挂载到本地的一个盘符或一个目录,在Windows上可以直接判断路径是否存在,而在Linux挂载点总是存在的,即使挂载失败也存在一个空的文件夹,所以不能用路径是否存在来判断,可以在df
命令中找挂载点。
1 | import os |
常见的背包问题根据背包的重数和最终求的值可以分为下面6类:
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 | dp[i][c] = max(dp[i-1][c], dp[i][c-1]); |
有时候要求背包必须是满的,则没有上述单调性,或者我们总是不考虑单调性,而在给出最终答案前去遍历dp[n-1]得到最大值,鉴于此我们把上式简化,并加上循环
1 | for(int i=0;i<n;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 | for(int i=0;i<n;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 | for(int i=0;i<n;i++) { |
循环完成后 max(dp)
就是背包能取得的最大价值。
完全背包的复杂度和0-1背包相同,算法时间复杂度O(n*C)
,空间复杂度O(C)
。
问题描述:
有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 | dp[0] = 1; |
对于多重背包,要明确一点,相同种类的背包视为相同背包(例如5个第i种背包,取3个只会形成1种方案,而不是5C3种方案),那么还是可以按二进制的方式组合。
问题描述:
有n
种物品,每种无穷个,一个容量为C的背包,第i种物品的体积是w[i]
,问恰好装满背包的方案数。
思路:
和完全背包类似思路,直接上代码:
1 | dp[0] = 1; |
1 | import math |
我们知道在Linux上的任务计划可以通过cron服务管理,cron服务设置任务运行时间有着自定义的时间表达式,形如
1 | # m h dom mon dow command |
这条记录表示在每周一到周五的08:58自动运行/root/run.sh
脚本,除去command,我们看到时间表达式有m, h, dom, mon, dow
几个部分,分别表示分钟、小时、每月几号、月份、星期几。几号和星期几通常是二选一,指定了一个另一个就会指定成*
表示任意,也可以都指定成*
,则表示每天。
dow
的1-5
表示周一到周五,也可以用逗号分割列举具体星期几,例如1,3,5
表示每周一三五,除了周几,其他时间项也都可以使用此表示法。
除了用-
表示范围,还可以用/
表示频率例如:
1 | # m h dom mon dow command |
表示每周一~周五,小时为9点到14点,从0分开始每5分钟运行一次脚本,这样每天第一次运行的时间是09:00,每天最后一次运行的时间是14:55,中间都是每隔5分钟运行一次。
一些程序允许使用扩展的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,这样通常是为了缓解多个任务同时启动的压力。
1 | from croniter import croniter |
有时候我们即需要上次触发时间,又需要下次触发时间,要么我们理解了迭代器的方式通过取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 | # 安装nvm |
1 | # 安装项目依赖 |
视需求情况安装
1 | apt install nodejs-dev node-gyp libssl1.0-dev |
其中pm2
可以直接启动js作为服务器运行
1 | # 启动项目 |
其中app.js
为服务器启动脚本,参考脚本如下:
1 | // app.js |
LVM是 Logical Volume Manager(逻辑卷管理)的简写,它是Linux环境下对磁盘分区进行管理的一种机制,可以将多个物理硬盘进行重新组织,划分成多个逻辑卷并可以动态改变卷大小(例如增加新的硬盘来扩展已有的逻辑卷)
功能 | 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,即逻辑卷
1 | pvcreate <后面可以是/dev/sdb这样的未分区整个盘也可以是/dev/sdb1这样的一个分区> |
例如:pvcreate /dev/sda
1 | vgcreate 卷组名 物理卷 |
例如:vgcreate vg_abc /dev/sda
向卷组中增加物理卷
1 | vgextend vg_abc /dev/sdb |
从卷组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 |
|
编译时会在以下路径查找:
通过编译选项-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_PATH
或LD_LIBRARY_PATH
环境变量
在CMakeLists.txt
中使用link_directories(${LIB_DIRS})
来指定,LIB_DIRS是用一个或多个空白字符分割的多个路径
在CMakeLists.txt
中使用指定rpath,方式如下
1 | target_link_libraries(${PROJECT_NAME} |
其中'$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)
因为安装前需要停止并删除所有容器,我并没有尝试过。