0%

之前在公司内网搭建了PPTPD服务器解决疫情期间远程办公从家里连接公司内网服务器的问题,但一直不太稳定,平时随便用用还行,偶尔断开可以重连,一直想换个方式搭建,之前尝试过其他搭建方式没有成功,后来恢复线下办公后需求不强了就没有继续尝试,今天发现在Ubuntu系统下基于Docker搭建相对简单,于是记录下来

使用Docker Hub上的kylemanna/openvpn镜像

kylemanna/openvpn - Docker Image | Docker Hub

拉取镜像

1
docker pull kylemanna/openvpn

设置卷名环境变量

取一个名称用来创建用于存放openvpn数据的docker volume,建议用ovpn-data-前缀,后面的example换成自己取的

1
OVPN_DATA="ovpn-data-example"

初始化数据

VPN.SERVERNAME.COM换成自己的IP地址或域名

1
2
3
docker volume create --name $OVPN_DATA
docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM
docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki

启动OpenVPN服务器

1
docker run --name=openvpn -v $OVPN_DATA:/etc/openvpn -d -p 1194:1194/udp --cap-add=NET_ADMIN kylemanna/openvpn

生成不带密码的客户端证书

CLIENTNAME换成自定义的客户端标识,可以取形如USERNAME@SERVERNAME方便管理

1
docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full CLIENTNAME nopass

导出包含证书的客户端配置文件

CLIENTNAME同样换成自定义的客户端标识,导出为当前目录的CLIENTNAME.ovpn文件

1
docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient CLIENTNAME > CLIENTNAME.ovpn

如果服务器端口做过NAT映射,可以打开CLIENTNAME.ovpn修改其中的端口号

作废客户端证书

先查看openvpn数据卷的所在位置

1
docker volume inspect ovpn-data-example

删除文件

1
rm ***/_data/pki/private/CLIENTNAME.key

编辑

1
vim ***/_data/pki/index.txt

删除CLIENTNAME所在的行

取消全局代理

默认配置会将全部对外流量通过VPN,如果想要指定仅访问公司局域网时走VPN,其他流量走默认路由不变,可以通过在.ovpn配置文件中追加

1
2
3
4
5
route-nopull
route-metric 150
route remote_host 255.255.255.255 net_gateway
route 172.19.0.0 255.255.0.0 net_gateway
route 192.168.1.0 255.255.0.0 vpn_gateway

.ovpn配置文件的结尾找到

1
# redirect-gateway def1

注释掉这一行

其中route-nopull表示不接受服务器对于修改路由表的推送,而可以按照我们之后指定的规则去修改。

我所在的内网网段是172.19.0.0/16,所以明确指定路由,走net_gateway,也就是原有网关。

192.168.1.0/24是需要走VPN的,所以也明确指定了路由,走vpn_gateway。

redirect-gateway def1作用是将默认网关重定向到 VPN 服务器上,所以要注释掉。

修改服务器配置

<openvpn容器卷路径>/_data/openvpn.conf

1
#push "block-outside-dns"

<openvpn容器卷路径>/_data/ovpn_env.sh

1
declare -x OVPN_DISABLE_PUSH_BLOCK_DNS=1

重启服务器,重连客户端

客户端

Windows 7

Windows 10

macOS v2

macOS v3

Ubuntu

安装openvpn

1
apt -y install openvpn

客户端和服务器会同时安装,我们不用服务器可以把它关掉

1
service openvpn stop

禁止openvpn服务器开启自启动

1
systemctl disable openvpn

用客户端配置文件在后台创建VPN连接

1
openvpn --daemon --config CLIENTNAME.ovpn

BTW:根据测试,同一份配置文件似乎不能在多个主机重复使用,如果使用会造成两边连接频繁断开。

最长公共前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 预处理字符串s,得到所有s任意后缀即s[i:]和s[j:]的所有最长公共前缀
class LCP {
vector<vector<int>> dp;
public:
LCP(const string& s) {
dp.resize(s.size()+1, vector<int>(s.size()+1));
for(int j=s.size()-1;j>=0;j--) {
for(int i=j;i>=0;i--) {
dp[i][j]=(s[i]==s[j]?dp[i+1][j+1]+1:0);
}
}
}

int operator()(int i, int j) const {
return dp[min(i,j)][max(i,j)];
}
};

这个算法本身本身就是动态规划,并不难,而有时这个问题会嵌套于另一个问题里,使整体变得略复杂,而如果能想到LCP问题可以用O(n^2)预处理,则复杂问题迎刃而解。

6195. 对字母串可执行的最大删除数 - 力扣(LeetCode)

一个朴素的解法是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int deleteString(string s) {
int n = s.size();
// 用dp[i] 表示删除s[i:]的最大操作次数
// 删除一个长度为j的前缀之后得到的是一个子问题:s[i+j:]的最大操作次数
// 能删除s[i:i+j]的前提是s[i:i+j]==s[i+j:i+j+j]
vector<int> dp(n, 1);
for(int i=n-2;i>=0;i--) {
for(int j=1;j<=(n-i)/2;j++) {
if(memcmp(&s[i], &s[i+j], j) == 0) { // s[i:i+j]==s[i+j:i+j+j]
dp[i] = max(dp[i], 1+dp[i+j]);
}
}
}
return dp[0];
}
};

如果将memcmp(&s[i], &s[i+j], j)视为O(n)的复杂度,那么算法整体是O(n^3)复杂度。

但如果我们预处理得到s所有后缀的最长公共前缀的长度,就可以判断长度是否>=j来代替判断子串相等

从而把O(n3)的复杂度降到O(n2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int deleteString(string s) {
int n = s.size();
// 用dp[i] 表示删除s[i:]的最大操作次数
// 删除一个长度为j的前缀之后得到的是一个子问题:s[i+j:]的最大操作次数
// 能删除s[i:i+j]的前提是s[i:i+j]==s[i+j:i+j+j]
LCP lcp(s);
vector<int> dp(n, 1);
for(int i=n-2;i>=0;i--) {
for(int j=1;j<=(n-i)/2;j++) {
if(lcp(i, i+j)>= j) { // s[i:i+j]==s[i+j:i+j+j]
dp[i] = max(dp[i], 1+dp[i+j]);
}
}
}
return dp[0];
}
};

起因:文件共享服务器原本开启的NFS共享,发现在Windows客户端访问并不稳定,想改用Samba共享,但是很多人在用NFS访问,想先同时开启两种共享同时支持访问,遇到的问题是通过NFS客户端创建出的文件权限是0755,通过Samba客户端不可写,而通过Samba客户端创建的文件可以通过create mask和 force create mask指定成任意的权限,可以保证NFS客户端可写,而且没有找到NFS如何配置可以控制新建文件的权限。于是想换个思路监视文件创建,在创建后立刻修改权限。

inotifywait 异步文件系统监控机制

inotify 一种强大的、细粒度的、异步文件系统监控机制,它满足各种各样的文件监控需要,可以监控文件系统的访问属性、读写属性、权限属性、删除创建、移动等操作,也就是可以监控文件发生的一切变化。
inotify-tools 是一个C库和一组命令行的工作提供Linux下inotify的简单接口。inotify-tools安装后会得到inotifywait和inotifywatch这两条命令:

inotifywait命令 可以用来收集有关文件访问信息,Linux发行版一般没有包括这个命令,需要安装inotify-tools,这个命令还需要将inotify支持编译入Linux内核,好在大多数Linux发行版都在内核中启用了inotify。
inotifywatch命令 用于收集关于被监视的文件系统的统计数据,包括每个 inotify 事件发生多少次。

安装

1
apt install inotify-tools

修改最大监控文件数

1
sysctl -w fs.inotify.max_user_watches="1000000"

可以通过下面的命令确认修改成功

1
cat /proc/sys/fs/inotify/max_user_watches

永久修改(重启后自动修改)

vim /etc/sysctl.conf

追加 fs.inotify.max_user_watches=1000000

编写监控脚本

1
2
3
4
5
6
7
#!/bin/bash
inotifywait -mrq --timefmt '%y-%m-%d %H:%M' --format '%T %e %w%f' -e create /data/share/ | while read DATE TIME EVENT FILEPATH
do
echo "${FILEPATH} was ${EVENT} at ${DATE}_${TIME}"
chmod 777 "${FILEPATH}"
done

此脚本监控/data/share目录下的create事件,一旦出现create事件则调用chmod把刚创建的文件权限修改为0777。

BTW:通常情况可以正常工作,实际测试时,如果在创建后立即重命名,可能会导致监控脚本来不及处理,在执行到chmod时文件已经不存在。

注意这里把%w%f拼在一起用FILEPATH作为最后一个变量读取出来是可以解决路径或文件名有空格的问题。

其他选项

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
inotifywait 3.14
Wait for a particular event on a file or set of files.
Usage: inotifywait [ options ] file1 [ file2 ] [ file3 ] [ ... ]
Options:
-h|--help Show this help text.
@<file> Exclude the specified file from being watched.
--exclude <pattern>
Exclude all events on files matching the
extended regular expression <pattern>.
--excludei <pattern>
Like --exclude but case insensitive.
-m|--monitor Keep listening for events forever. Without
this option, inotifywait will exit after one
event is received.
-d|--daemon Same as --monitor, except run in the background
logging events to a file specified by --outfile.
Implies --syslog.
-r|--recursive Watch directories recursively.
--fromfile <file>
Read files to watch from <file> or `-' for stdin.
-o|--outfile <file>
Print events to <file> rather than stdout.
-s|--syslog Send errors to syslog rather than stderr.
-q|--quiet Print less (only print events).
-qq Print nothing (not even events).
--format <fmt> Print using a specified printf-like format
string; read the man page for more details.
--timefmt <fmt> strftime-compatible format string for use with
%T in --format string.
-c|--csv Print events in CSV format.
-t|--timeout <seconds>
When listening for a single event, time out after
waiting for an event for <seconds> seconds.
If <seconds> is 0, inotifywait will never time out.
-e|--event <event1> [ -e|--event <event2> ... ]
Listen for specific event(s). If omitted, all events are
listened for.

Exit status:
0 - An event you asked to watch for was received.
1 - An event you did not ask to watch for was received
(usually delete_self or unmount), or some error occurred.
2 - The --timeout option was given and no events occurred
in the specified interval of time.

Events:
access file or directory contents were read
modify file or directory contents were written
attrib file or directory attributes changed
close_write file or directory closed, after being opened in
writable mode
close_nowrite file or directory closed, after being opened in
read-only mode
close file or directory closed, regardless of read/write mode
open file or directory opened
moved_to file or directory moved to watched directory
moved_from file or directory moved from watched directory
move file or directory moved to or from watched directory
create file or directory created within watched directory
delete file or directory deleted within watched directory
delete_self file or directory was deleted
unmount file system containing file or directory unmounted

文件共享主流协议:NFS/CIFS,Samba是Linux上创建CIFS共享的工具

NFS是Linux间常用的共享协议

Linux服务端需要安装nfs-kernel-server,服务名称nfs-kernel-server别名nfs-server,依赖服务rpcbind,共享配置文件/etc/exports

​ NFS 服务器需要端口 111 2049 4045-4049

Linux客户端需要安装nfs-common,通过mount命令挂载使用,实际上nfs-common安装出的是mount的子命令mount.nfs

Windows通常不用做NFS服务端,不清楚是否支持

Windows客户端“启用或关闭Windows功能”中找到“NFS客户端”勾选并安装,通过mount命令挂载使用。

配置NFS服务器

通过NFS挂载的共享,如果指定了no_all_squash则权限按照与客户端登录的用户id相同id的服务端用户的权限计算,简直是乱来,所以我觉得应该始终指定
all_squash, root_squash,让所有访问者按照nobody:nogroup的权限计算

只读共享参考:

1
/data *(insecure,ro,async,all_squash,root_squash,no_subtree_check)

读写共享参考:

1
/data 192.168.1.123(insecure,rw,sync,all_squash,root_squash,no_subtree_check) 192.168.1.124(insecure,rw,sync,all_squash,root_squash,no_subtree_check)

NFS客户端挂载命令

1
mount 172.17.0.2:/data <挂载点>

CIFS是Windows默认的共享协议

Windows服务端依赖Windows的Server服务(默认存在并自动启动)

Windows客户端通过net use 命令使用,或者打开“此电脑”-计算机菜单-映射网络驱动器,或者直接在文件夹的地址栏输入共享路径即可。

Linux服务端需要安装samba,服务名称smbd,共享配置文件/etc/samba/smb.conf

​ samba 服务器需要端口 137-139 445

Linux客户端需要安装cifs-utils,通过mount命令挂载使用,实际上cifs-utils安装出来的是mount的子命令mount.cifs

配置Samba服务器

首先需要添加Samba服务器的用户,这个用户同时必须已经是系统用户,这里我们新建系统用户samba

1
useradd samba

将samba添加为Samba服务器用户

1
smbpasswd -a samba

在配置文件/etc/samba/smb.conf追加

1
2
3
4
5
6
7
8
9
10
11
12
13
[data]
common = Shared Data
path = /data # 共享路径
workgroup = WORKGROUP # 和WINDOWS工作组一致,从局域网内其他WINDOWS系统的属性可以看到
public = yes # 是否允许游客访问
guest account = nobody # 游客用户
browseable = yes # 是否允许列出文件
writable = yes # 是否可写
write list = samba # 具有写权限的用户(多用户空格分割)
create mask = 0777 # 创建文件时赋权,相当于执行chmod
directory mask = 0777
force user = nobody # 创建文件时强制所有者,相当于执行chown
force group = nogroup

Samba客户端挂载命令

游客

1
mount //172.17.0.2/data <挂载点> -ouser=guest,pass=

其他用户

1
mount //172.17.0.2/data <挂载点> -ouser=samba,pass=<密码>

在Windows 上可以用net use来挂载Linux的Samba共享就像挂载Windows的共享一样。

1
net use X: \\172.17.0.2\data <密码> /user:samba

PS:虽然Windows可以安装NFS客户端来访问Linux的NFS共享,但是不稳定,推荐还是在Linux用samba做共享,如果只在Linux间共享可以用NFS。而且samba有着更完善的权限控制,可以用配置文件指定通过共享创建的文件或文件夹的owner和权限,并且可以通过带有通配符的ip指定访问黑名单或白名单,同时允许通过密码访问或仅限制IP访问。而NFS似乎只能通过IP指定访问权限而不能通过用户+密码的方式,并且只能指定创建出文件的owner不能指定权限。

我们知道对于线性表,有两种基础的数据结构:数组和链表。

对于数组而言,支持O(1)的随机访问,结尾插入删除不考虑扩容可以近似认为是O(1),而中间插入或删除则需要O(n)的时间移动插入点后的所有数据。

对于链表而言,不支持随机访问,访问第k个元素需要从头遍历需要O(n)的时间,而插入和删除只需要改变next指针即O(1)的时间,当然定位到插入删除点还是需要O(n)的时间。

如果我们把数组和链表结合起来,有链表连接大块的数组而不是单个元素,那么访问、插入、删除都会折中,有时这很有用,我们先思考这个大块数组要取多大合适,如果数据的最大长度是n,我们块大小取k,则会形成n/k个块,那么定位到任意一个元素的最坏时间是n/k(即在链表的最后一个结点上,需要从头遍历的时间),而插入删除一个元素的最坏时间是k,即在大块的头上插入需要移动整个大块的数据。

k越大n/k越小,如果我们折中,显然是让k=n/k,即k=sqrt(n)。那么上述操作的复杂度最坏情况下都是O(sqrt(n))。

思路就是这么简单,还有一些实现细节,插入时考虑分裂大块,删除时考虑合并大块,具体直接看代码吧。

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

//一个节点的初始大小
const int NODESIZE=160; //如果数据大小最大为n,取在sqrt(n)/2会比较好,对于10w的数据这个大小应该比较优
//加上缓冲区一个节点应该占用NODESIZE*2的空间
//任何一个节点大小不会>=NODESIZE*2,任何两个相邻的节点大小不会<=NODESIZE

template<typename T>
class BlockList{
public:
struct BLNode{
T data[NODESIZE<<1];
int size;//该结点中已经使用的大小
BLNode* next;
BLNode():size(0),next(NULL){}
};
BLNode* _head;
BLNode* _tail;
int _size; //整个表中的元素数

void split(BLNode* p){
BLNode* q = new BLNode();
q->next = p->next;
p->next = q;
memcpy(q->data, p->data+NODESIZE, sizeof(T)*(p->size-NODESIZE));
q->size = p->size-NODESIZE;
p->size = NODESIZE;
if(_tail == p) _tail = q;
}

void join(BLNode* p, BLNode* q) {
memcpy(p->data+p->size, q->data, sizeof(T)*q->size );
p->size += q->size;
p->next = q->next;
if(_tail == q) _tail = p;
delete q;
}
public:
BlockList(int size=0, const T& val=T()):_size(size) {
_head = _tail = new BLNode();
if(size != 0) {
while(size>NODESIZE){
std::fill(_tail->data, _tail->data+NODESIZE, val);
size -= NODESIZE;
_tail->size = NODESIZE;
if(!_tail->next) {
_tail->next = new BLNode();
}
_tail = _tail->next;
}
std::fill(_tail->data, _tail->data+size, val);
_tail->size = size;
}
}
int size(){return _size;}

//通过下标随机访问,复杂度O(sqrt(size))
T& operator [] (int i){
assert(0<=i && i<_size);
BLNode* p = _head;
while(i>=p->size){
i -= p->size;
p = p->next;
}
return p->data[i];
}

//在尾部追加数据
void push_back(const T& d) {
_size++;
_tail->data[_tail->size++] = d;
if(_tail->size==(NODESIZE<<1)){
split(_tail);
}
}

//在下标i处插入数据d,原数据依次后移
void insert(int i, const T& d){
assert(0<=i && i<=_size);
_size++;
BLNode* p=_head;
while(i>p->size){
i -= p->size;
p = p->next;
}
for(int j=p->size-1; j>=i; j--){
p->data[j+1]=p->data[j];
}
p->data[i]=d;
p->size++;
if(p->size==(NODESIZE<<1)){
split(p);
}
}

//删除在下标i处的数据,后续数据依次前移
void erase(int i){
assert(0<=i && i<_size);
_size--;
BLNode* p=_head;
while(i>=p->size){
i -= p->size;
p = p->next;
}
for(int j=i;j<p->size-1;j++){
p->data[j]=p->data[j+1];
}
p->size--;
BLNode* q = p->next;
if(q && p->size+q->size<=NODESIZE){
join(p, q);
}
}
};

406. 根据身高重建队列

这道题可以把人的身高从高到低排序,然后依次根据ki插入到数组的ki位置就可以了,如果用数组实现就是O(n2)的,改用块状链表可以降到O(n1.5)

把初始结点大小取在16~32有可能跑出击败100%用户的时间,sqrt(2000)/2是22左右。

1
2
3
4
5
6
执行用时:
12 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:
11.6 MB, 在所有 C++ 提交中击败了82.61%的用户
通过测试用例:
36 / 36

珂朵莉树(ChthollyTree)又称老司机树(ODT),起源于CF 896C Willem, Chtholly and Seniorious

一位用户Old Driver在给出了线段树的解法后,又发布了一份前所未有的解法,其中利用到的数据结构因此被称为珂朵莉树或老司机树。

其核心就是用二叉搜索树维护一组连续的不相交的区间,初始是覆盖了整个数轴的一个大区间,核心操作只有两个一是split(pos)将整个区间集在pos处断开并返回以pos为左端点的迭代器,assign(l, r, val)将区间[l,r)赋值为val。

先上代码

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
typedef long long ll;

template<typename T>
class ChthollyTree {
private:
struct ChthollyNode {
ll l, r;
mutable T w;
ChthollyNode(ll l, ll r=0, const T& w=T()): l(l),r(r),w(w) {}
bool operator < (const ChthollyNode& n) const {
return l < n.l;
}
};
set<ChthollyNode> s;
public:
ChthollyTree(ll n=1e9+2, const T& v=T()) { s.insert(ChthollyNode(-1, n, v)); }

auto split(ll pos) {
auto itr = s.lower_bound(pos);
if (itr != s.end() && itr->l == pos) return itr;
--itr;
auto l = itr->l;
auto r = itr->r;
auto w = itr->w;
s.erase(itr);
s.insert(ChthollyNode(l, pos, w));
return s.insert(ChthollyNode(pos, r, w)).first;
}

void update(ll l, ll r, function<void(ChthollyNode&)> f) {
auto right = split(r), left = split(l);
for(auto itr = left; itr != right; itr++) {
f(const_cast<ChthollyNode&>(*itr));
}
}

void assign(ll l, ll r, const T& w) {
auto right = split(r), left = split(l);
s.erase(left, right);
s.insert(ChthollyNode(l, r, w));
}

void visit(ll l, ll r, function<void(const ChthollyNode&)> f) {
auto left = s.lower_bound(l);
if (left == s.end() || left->l != l) --left;
auto right = s.lower_bound(r);
for(auto itr = left; itr != right; itr++) {
f(*itr);
}
}

private:
class ItemAccessor {
public:
ItemAccessor(ChthollyTree& ct, ll i): ct(ct), i(i) {}
ItemAccessor& operator = (const T& x) {ct.assign(i, i+1, x);return *this;}
ItemAccessor& operator += (const T& x) {ct.update(i, i+1, [x](auto& p){p.w += x;});return *this;}
ItemAccessor& operator -= (const T& x) {ct.update(i, i+1, [x](auto& p){p.w -= x;});return *this;}
operator T() const {T r;ct.visit(i, i+1, [&r](auto& p){r = p.w;}); return r;}
private:
ChthollyTree& ct;
ll i;
};

class RangeAccessor {
public:
RangeAccessor(ChthollyTree& ct, ll b, ll e):ct(ct), b(b), e(e) {}
void operator = (const T& x) {ct.assign(b, e, x);}
void operator += (const T& x) {ct.update(b, e, [&x](auto& p){p.w += x;});}
void operator -= (const T& x) {ct.update(b, e, [&x](auto& p){p.w -= x;});}
private:
ChthollyTree& ct;
ll b;
ll e;
};

public:
// 通过下标访问单点值的语法糖
ItemAccessor operator [] (ll i) {
return ItemAccessor(*this, i);
}

// 通过下标访问区间值的语法糖
RangeAccessor operator [] (const tuple<ll, ll>& range) {
return RangeAccessor(*this, std::get<0>(range), std::get<1>(range));
}

T sum(ll l, ll r) {
T ret = 0;
visit(l, r, [&](auto& p) {
ret += p.w * (std::min(p.r, r)-std::max(p.l, l));
});
return ret;
}

T min(ll l, ll r) {
bool first = true;
T ret = 0;
visit(l, r, [&](auto& p) {
if(first) {
first = false;
ret = p.w;
} else {
ret = std::min(ret, p.w);
}
});
return ret;
}

T max(ll l, ll r) {
bool first = true;
T ret = 0;
visit(l, r, [&](auto& p) {
if(first) {
first = false;
ret = p.w;
} else {
ret = std::max(ret, p.w);
}
});
return ret;
}
};

解释一下几个关键点:

1)ChthollyNode结构体中数据字段w为什么用mutable修饰,原本在对set元素遍历时是不能修改元素值的,因为set底层是一颗红黑树,如果修改值可能导致它不满足二叉搜索树的性质了。但这里比较特殊,这里的区间是不重叠的,仅通过左端点的值来定义结点的序,所以修改w不会影响到序。

2)update和assign的时候为什么都是先取右端迭代器再取左端迭代器auto right = split(r), left = split(l);,因为split可能导致迭代器失效,如果先取了左端点迭代器,右端点可能就在左端迭代器指向的节点上,将它断成两个结点后原迭代器也就是已经拿到的左端点迭代器就失效了。

我在其上进一步实现了常见的区间求和,区间最大最小值,区间赋值,区间自增,单点赋值,单点自增,单点取值。可以发现它和线段树完成了很多相似的操作,但是实现要比线段树简单很多。

更复杂的自定义操作可以通过update和visit传入自定义的操作方式,其中update会调用split断开传入的[l, r)端点,以便执行数据更新,而visit则不会断开区间,只保证迭代的首位区间能覆盖[l, r),可以用于查询时取数据进行计算。

关于效率,我们可以看到查询操作通过visit完成,不会增加珂朵莉树的复杂程度,而更新操作分两种,一种是区间的统一赋值,通过assign完成通常情况下会减少整体区间的数量,因为它会删除[l, r)上的区间合并成一个,而update操作可能会增加1个或2个区间取决于左右端点是否已经存在。

如果操作和数据是随机的,并且穿插着assign操作,那么珂朵莉树的实际效率会很好,如果操作只用到update和visit,那珂朵莉树会越来越复杂,不过如果查询只是单点仍然可以提供比较好的效率。我实际测试通常要比动态开点线段树的耗时少,甚至在只有update和区间查询最值时实际效率也不差,不明所以。

另外,我们还可以通过珂朵莉树完成线段树不方便进行的操作,比如查询区间内元素的方差。

问题描述

RMQ问题即区间最值查询问题(Range Minimum/Maximum Query)

给定一个数组,大量的查询区间的最值,我们知道如果数组长度是n,有n*(n-1)/2个不同区间,如果只有少量的查询我们可以遍历区间的元素,算法的时间复杂度显然是O(len) len是区间长度,对于随机的区间一次查询的复杂度也就是O(n)。

如果查询的次数增加到n2数量级,我们可以用递推预处理出所有区间的最值,预处理时间O(n2),查询时间O(1)。

如果查询的次数和n是同等量级,无论我们采取遍历还是预处理所有区间总体时间都是O(n^2),但在这种情况下其实有更高效的解决方案。

以查询最大值为例,对于长度为n的数组a,

dp(i,j)表示从ai起连续2j个元素的最大值dp(i, j) 表示从a_i起连续2^j个元素的最大值

即区间[i,i+2j1]的最大值即区间[i, i+2^j-1] 的最大值

长度为2j区间的最大值可以通过两个长度2{j-1}的区间最大值得到
所以有状态方程

dp(i,j)=max(dp(i,j1),dp(i+2j1,j1))dp(i, j) = max(dp(i, j-1), dp(i+2^{j-1},j-1))

对于i=0,1,…,n-1,j取值范围满足 i+2^j<n,即

j<log2(ni)j< log_2(n-i)

初始化

我们可以通过递推预处理dp所有的元素,时间复杂度O(nlogn)

查询

查询区间[i,j]的最大值记作query(i,j),我们还是可以利用拆分区间的思路把[i,j]拆成两个长度为2^k的区间这样就可以在预处理的dp表里直接找到了。

rmq-st

其中

k=log2(ji+1)k = \lfloor log_2(j-i+1) \rfloor

query(i,j)=max(dp(i,k),dp(j2k+1,k))query(i,j) = max(dp(i, k), dp(j-2^k+1,k))

代码

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
template<typename T, typename Cmp=less<int>>
class RMQ {
vector<vector<T>> dp; //dp[i][j]表示从a[i]起连续2^j次方个数的最值

static int ceil_log2(int n) {
int r = 0;
while((1<<r)<n) r++;
return r;
}

static int floor_log2(int n) {
int r = 0;
while((1<<r)<=n) r++;
return r-1;
}

int max_or_min(const T& a, const T& b) const {
return Cmp()(a, b)?a:b;
}

public:
RMQ() = default;
RMQ(const T* a, int n) {init(a, n);}
RMQ(const vector<T>& a) {init(&a[0], a.size());}

void init(const T* a, int n) {
dp.resize(n, vector<T>(ceil_log2(n)+1));
for(int i=0;i<n;i++){
dp[i][0] = a[i];
}
for(int j=1,s=1;s<n;s=(1<<j++)){
for(int i=0;i+s<n;i++){
dp[i][j] = max_or_min(dp[i][j-1], dp[i+s][j-1]);
}
}
}

//返回[i, j]范围内的最值
T query(int i, int j) const {
assert(i<=j);
int k = floor_log2(j-i+1);
return max_or_min(dp[i][k], dp[j-(1<<k)+1][k]);
}
};

template<typename T>
using RMinQ = RMQ<T, less<T>>;
template<typename T>
using RMaxQ = RMQ<T, greater<T>>;

完整样例

一份CMakeLists.txt完整的样例和解释说明

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
cmake_minimum_required(VERSION 3.13)
project(ust_demo CXX C)

# 通过上面的PROJECT设置后自动就有了PROJECT_NAME变量
message(STATUS PROJECT_NAME=${PROJECT_NAME})
# 指向顶级CMakeLists.txt所在绝对路径
message(STATUS CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR})
# CMAKE_CURRENT_SOURCE_DIR变量是指向当前CMakeLists.txt所在的绝对路径
message(STATUS CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR})
# 指向最后一次调用project命令时CMakeLists.txt所在的绝对路径
message(STATUS PROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR})
# 只要单个CMakeLists.txt时上面三个变量没有不同,当有多级目录多个CMakeLists.txt通过add_subdirectory相互包含时会有区别

# 在cmake命令中加参数 -DCMAKE_BUILD_TYPE=Debug 来指定编译Debug版本,否则默认编译Release版本
IF (CMAKE_BUILD_TYPE MATCHES "Debug" OR CMAKE_BUILD_TYPE MATCHES "DEBUG")
set(CMAKE_BUILD_TYPE "Debug")
# 指定可执行文件输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/out/Debug)
file(MAKE_DIRECTORY ${EXECUTABLE_OUTPUT_PATH})
ELSE ()
set(CMAKE_BUILD_TYPE "Release")
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/out/Release)
file(MAKE_DIRECTORY ${EXECUTABLE_OUTPUT_PATH})
ENDIF ()
# 指定存放动态链接库的路径
# 生成可执行文件后可以把out文件夹下的文件整体复制到其他环境下运行,为了保证动态链接库能找到需要做两件事:
# 1.把.so文件复制到out/libs目录下
# 2.链接时通过-rpath选项指定运行时查找链接库的路径(这一步如果不指定也可以在运行前通过LD_LIBRARY_PATH环境变量指定)
set(LIBRARY_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH}/libs)
file(MAKE_DIRECTORY ${LIBRARY_OUTPUT_PATH})

# 设置编译选项
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")

set(CMAKE_C_FLAGS_DEBUG " -std=c99 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG")
set(CMAKE_CXX_FLAGS_DEBUG " -std=c++17 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG")

set(CMAKE_C_FLAGS_RELEASE " -std=c99 -O3 -Wall -Wno-unused-function -Wno-unused-result -Wno-unknown-pragmas -Wno-format-security -fpic -fPIC")
set(CMAKE_CXX_FLAGS_RELEASE " -std=c++17 -O3 -Wall -Wno-unused-function -Wno-unused-result -Wno-unknown-pragmas -Wno-format-security -fpic -fPIC")

# 添加头文件目录
set(INCLUDE_DIR ./include)
include_directories(${INCLUDE_DIR})

# 添加库文件目录
set(LIB_DIR lib/linux.x64)
link_directories(${LIB_DIR})

# 将src目录下所有源文件加入 SRC_LIST
aux_source_directory(./src SRC_LIST)
# # 也可以通过 FILE(GLOB_RECURSE <var> <parterns>) 把匹配指定模式的文件加入变量中
# 区别是这样同时会查找src子目录下的.cpp和.c文件
# file(GLOB_RECURSE SRC_LIST "src/*.cpp" "src/*.c")
# # 也可以通过set明确地逐个文件加入源文件列表
# set(SRC_LIST
# src/main.cpp
# )

# 指定生成可执行文件要编译的源文件
add_executable(${PROJECT_NAME}
${SRC_LIST}
)

# 指定生成.so动态库要编译的源文件
#add_library(${PROJECT_NAME} SHARED
# ${SRC_LIST}
# )
# 指定生成.a静态库要编译的源文件
#add_library(${PROJECT_NAME}
# ${SRC_LIST}
# )

# 将指定的.so文件的绝对路径加入 LIB_LIST
# file(GLOB_RECURSE LIB_LIST "${LIB_DIR}/*.so")

# 同样也可以通过set明确地逐个文件加入库文件列表
set(LIB_LIST
libHSSecuTradeApi.so
t2sdk
)

file(COPY ${LIB_DIR}/libHSSecuTradeApi.so DESTINATION ${LIBRARY_OUTPUT_PATH})
file(COPY ${LIB_DIR}/libt2sdk.so DESTINATION ${LIBRARY_OUTPUT_PATH})

# 指定指定生成PROJECT_NAME要连接的动态链接库
# 这里链接库的路径可以是绝对路径
# 也可以只写文件名将在通过link_directories指定的包含目录以及环境变量LIBRARY_PATH指定的目录下去查找
# 名为libxxx.so的文件可以只写xxx
target_link_libraries(${PROJECT_NAME}
${LIB_LIST}
pthread
-Wl,-rpath,'$ORIGIN'/libs
)
# -Wl 的意思是把后面参数传给链接器,多个参数用逗号分割,这里实际传给链接器的参数是 -rpath '$ORIGIN'/libs
# 其中$ORIGIN表示运行时可执行文件的位置,'$ORIGIN'/libs也就是可执行文件同目录的libs文件夹。
# 不能指定成./libs因为这会被误认为是链接时的相对路径,而不是运行时的。

# 指定PROJECT_NAME需要用到的头文件路径
# 只在目标是库文件时有意义,此项目是一个子项目时,其他项目链接这个项目生成的库时会自动添加此处指定的INCLUDE路径
target_include_directories(${PROJECT_NAME} PUBLIC
include
../include
)

# 指定PROJECT_NAME需要用到的库文件路径
# 只在目标是库文件时有意义,此项目是一个子项目时,其他项目链接这个项目生成的库时会自动添加此处指定的LINK路径(需要CMake 3.13)
target_link_directories(${PROJECT_NAME} PUBLIC ${LIB_DIR})

#file(MAKE_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)
## 指定可执行文件输出路径
#set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
#file(MAKE_DIRECTORY ${PROJECT_SOURCE_DIR}/bin/libs)
## 指定库文件输出路径
#set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin/libs)

## 递归处理含子文件下的CMakeLists.txt,第二个参数是指定其中间文件路径相对当前文件的路径,如果不指定则会把中间文件生成在子文件夹下
#ADD_SUBDIRECTORY(subproj subproj)

简洁模板

简洁版,可以基于这个改写

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
cmake_minimum_required(VERSION 3.10)
project(demo CXX C)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")

set(CMAKE_C_FLAGS_DEBUG " -std=c99 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG")
set(CMAKE_CXX_FLAGS_DEBUG " -std=c++17 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG")

set(CMAKE_C_FLAGS_RELEASE " -std=c99 -O3 -Wall -Wno-unused-function -fpic -fPIC")
set(CMAKE_CXX_FLAGS_RELEASE " -std=c++17 -O3 -Wall -Wno-unused-function -fpic -fPIC")

include_directories(
include
)
link_directories(
lib
)

aux_source_directory(src SRC_LIST)
add_executable(${PROJECT_NAME}
${SRC_LIST}
)

target_link_libraries(${PROJECT_NAME}
pthread
)

使用方式

cmake命令生成Makefile时会生成一些中间文件和一些缓存文件,我一般会在根CMakefile.txt的同级目录下新建build目录用来构建,通常的步骤是

1
2
3
4
mkdir build
cd build
cmake ..
make

cmake生成Makefile时是增量处理的,如果想重新生成,需要先删除CMakeCache.txt

make clean可以清除目标文件以保证再次make时是重新生成的。

在Windows上如果生成想用minGW的Makefile需要使用-G"Unix Makefiles"参数,即

1
2
cmake -G"Unix Makefiles" ..
make

记得用minGW/bin/mingw32-make.exe 复制一份make.exe,并把minGW/bin添加到PATH

构建后自动复制文件

有时候我们想将依赖的头文件(构建.so时需要依赖)或库文件复制到指定路径

可以使用如下命令

1
2
3
4
5
# 复制指定目录下的文件到指定的输出目录(包含子目录)
add_custom_command(TARGET <项目名> POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
<源路径> <目标路径>
)
1
2
3
4
5
6
7
8
9
# 复制指定目录下的*.h到指定的输出目录(不包含子目录)
file(GLOB HEADER_FILES "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h")
foreach(HEADER_FILE ${HEADER_FILES})
add_custom_command(TARGET amaquote POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${HEADER_FILE}
${OUTPUT_PATH}/include
)
endforeach()

执行Shell命令并保存输出结果到变量中

1
2
3
4
5
EXECUTE_PROCESS(COMMAND <Shell命令>
TIMEOUT 5
OUTPUT_VARIABLE <变量名>
OUTPUT_STRIP_TRAILING_WHITESPACE
)

样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
EXECUTE_PROCESS(COMMAND sh ${CMAKE_SOURCE_DIR}/is_abi_gcc4.sh
TIMEOUT 5
OUTPUT_VARIABLE IS_ABI_GCC4
OUTPUT_STRIP_TRAILING_WHITESPACE
)
IF (IS_ABI_GCC4 STREQUAL "1")
message(STATUS "Use gcc4-compatible libs")
SET(ABI_VERSION gcc4-compatible)
SET(LIB_DIR utils/lib/linux.x64/abi-gcc4-compatible)
ELSE()
message(STATUS "Use cxx11 libs")
SET(ABI_VERSION cxx11)
SET(LIB_DIR utils/lib/linux.x64/abi-cxx11)
ENDIF ()

写在COMMAND后面的命令支持带参数但不支持管道,如果需要用到管道可以写在外部的.sh中再进行调用

1
2
# is_abi_gcc4.sh
g++ -v -x 2>&1|grep gcc4-compatible|wc -l

起因:在公司服务器上装了Windows和Ubuntu双系统,Windows是自带的,Ubuntu是后装的。系统有两块硬盘,参照网上的教程从C盘和D盘分别压缩出200M和100G空间分给Ubuntu使用,安装时手动设置把200M挂载到/boot,把100G挂载到/,安装一切顺利,重启后可以看到Ubuntu带的Grub菜单,默认进入Ubuntu,还有一个选项是Windows Boot Manager可以进入Windows,选择Ubuntu进入Ubuntu正常,再次重启在Grub菜单选择进入Windows也正常,但是再次重启时Grub菜单消失了,直接进入了Windows,无法选择进入Ubuntu。

猜测是Windows Boot Manager会改写MBR(主引导记录)

MBR是硬盘上的一个扇区,包含了引导程序(的第一阶段)、分区表等信息。

引导程序第一阶段需要去MBR中去读引导程序,GRUB第二阶段需要到/boot分区读系统内核和配置文件。

而MBR中的程序包括了加载第二阶段的程序。

当我只有Windows系统时,MBR中的程序会启动Windows Boot Manager,在我安装了Ubuntu之后,MBR中的程序被修改成启动Ubuntu新分出来的那个/boot下的引导程序,然后可以通过菜单再去启动原来的Windows Boot Manager。

如果启动Windows Boot Manager后它修改了MBR让其中的程序在下次启动时加载自己,那么就跳过了选择进入Ubuntu的可能。

因为我们装Ubuntu只是为了测试,所以采取了临时的解决方式。通过Ubuntu的安装U盘启动,到选择安装Ubuntu的Grub菜单界面按c进入命令模式。

因为这时候插着一个U盘两块硬盘,不知道对应的编号,通过

1
2
3
ls (hd0)
ls (hd1)
...

这样的试探操作,可以根据硬盘上的分区数量和分区大小确定Ubuntu /boot对应的硬盘和分区编号

1
2
(hd1, gpt3) 100G Ubuntu /
(hd2, gpt5) 200M Ubuntu /boot

手动指定加载已经安装的Ubuntu的Grub的配置文件/boot/grub/grub.cfg

1
configfile  (hd2, gpt5)/grub/grub.cfg

会弹出选择菜单,就和MBR在引导第二阶段加载了Ubuntu的/boot一样,然后选择Ubuntu进入即可。

BTW:准备尝试还没有尝试的命令

1
2
3
set root=(hd2, gpt5)
chainloader /efi/EFI/ubuntu/grubx64.efi
boot

给定二维平面内的一组点集,求出包住所有点的最小的凸多边形。

1
2
输入: [(1, 1), (2, 2), (2, 0), (2, 4), (3, 3), (4, 2)]
输出: [(4, 2), (2, 4), (1, 1), (2, 0)]

算法思路是先排序(优先按x轴),然后分别求出上下包围线,从左到右遍历的过程使用单调栈维护包围线,包围线只能向同一侧弯曲(上包围线向下弯曲,上包围线向下弯曲),用叉乘计算连续两个线段的弯曲方向(二维向量其实是拟叉乘,三维向量的叉乘结果应该是一个向量,屏幕上两向量叉乘是一个指向屏幕外或屏幕内的向量,这里只用一个带正负的值表示方向和长度,其实我们这里也只用到方向)

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
// 二维向量
struct v2d {
double x;
double y;
};

// 二维向量拟叉积运算
double pseudoCross(const v2d& v1, const v2d& v2) {
return v1.x * v2.y - v1.y * v2.x;
}

v2d operator - (const v2d& v1, const v2d& v2) {
return v2d{v1.x-v2.x, v1.y-v2.y};
}

// 返回一个逆时针围成的凸包(如果要顺时针可以把上下边界换一换,改变while中的比较就可以)
vector<v2d> convexHull(vector<v2d>& pnts) {
sort(pnts.begin(), pnts.end(), [](const v2d& v1, const v2d& v2) {
return ((v1.x != v2.x)?(v1.x< v2.x):(v1.y < v2.y));
});
int n = pnts.size();
vector<int> v(n*2); // v 其实是两个单调栈在中间接起来
int p = n; // 从v中间向后增长的单调栈, p为栈顶的下一个位置(将要被使用的空间)
int q = n; // 从v中间向前增长的单调栈, q为栈顶的下一个位置(将要被使用的空间)
for(int i=0; i<n; i++){
while(p-n>=2 && pseudoCross(pnts[i]-pnts[v[p-1]], pnts[v[p-1]]-pnts[v[p-2]])>=0){
p--; // 弯曲方向不对,出栈
}
while(n-q>=2 && pseudoCross(pnts[i]-pnts[v[q+1]], pnts[v[q+1]]-pnts[v[q+2]])<=0){
q++; // 弯曲方向不对,出栈
}
v[p++] = i;
v[q--] = i;
}
vector<v2d> res;
for(int i=q+1;i<p-1;i++) {
res.push_back(pnts[v[i]]);
}
return res;
}