跳转至

Welcome

安装wordpress记录

本文记录在debian上安装配置wordpress的补助,并配置了ftp功能,支持自更新wordpress。

安装wordpress

下载最新的包,解压到/var/www/html

安装配置数据库

安装mariadb, 创建数据库和用户

mysql -u root
CREATE DATABASE wordpressdb; 
CREATE USER wordpressuser@localhost IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON wordpressdb.* TO wordpressuser@localhost;

安装配置nginx php

安装nginx和php-7.4,还有php一些组件 nginx 配置

server { 
  listen 80 default_server; 
  listen [::]:80 default_server;
  server_name your_domain.com;

  root /var/www/html;
  index index.php index.html index.htm;

  location / {
    # try_files $uri $uri/ =404;
    try_files $uri $uri/ /index.php?q=$uri&$args;
  }

  location ~ \.php$ {
      include snippets/fastcgi-php.conf;
      fastcgi_split_path_info ^(.+\.php)(/.+)$;
      fastcgi_pass unix:/run/php/php-fpm.sock;
  }
}

安装配置vsftpd

安装后,修改配置文件/etc/vsftpd.conf

local_enable=YES
write_enable=YES
chroot_local_user=YES
chroot_list_enable=YES
chroot_list_file=/etc/vsftpd.chroot_list
allow_writeable_chroot=YES
pm_service_name=ftp

添加用户,并限制权限

useradd [user_name]
passwd [user_name]
usermod -d /var/www/html/wordpress user_name -s /sbin/nologin

setfacl -m u:ftpsecure:r-x /var/www/html/

setfacl -R -m u:ftpsecure:rwx /var/www/html/wordpress
setfacl -R -d -m u:ftpsecure:rwx /var/www/html/wordpress
如果提示没有setfacl命令,则执行安装acl命令 apt install acl

ftp遇到问题和解决办法

  • 登陆提示"500 OOPS: could not read chroot() list file:/etc/vsftpd.chroot_list",需要创建文件/etc/vsftpd.chroot_list

  • 登陆ftp报530验证失败的问题,修改配置如下

    pm_service_name=ftp
    

  • 登陆提示"500 OOPS: vsftpd: refusing to run with writable root inside chroot()
allow_writeable_chroot=YES
  • 登陆提示 500 OOPS: vsftpd: refusing to run with writable root ,意思是登陆者具备对目录的写权限,这是由于目录设定权限为777,所以要是重新设定为755
    chmod 755 /srv/ftp
    

配置wordpress

修改wordpress/wp-config.php 文件, 添加如下内容

define('FTP_HOST', 'localhost');
define('FTP_USER', 'ftp');
define('FTP_PASS', '');
通过wordpress的api可以生成随机token,将输出替换到wordpress/wp-config.php

$curl -s https://api.wordpress.org/secret-key/1.1/salt/

define('AUTH_KEY',         'uOeyPW$K[P}23x^0l##N+qP#xzUlIBV[ZQlATs!7J?+5^!w0*bgEw|V6)k:YU0en');
define('SECURE_AUTH_KEY',  'Ad*-`eK|U4Z*7g}7CdX<q0^EuGqT1Tt}CaDRgF%NX-|fmk(:BACtws+^_0Pb8ZRA');
define('LOGGED_IN_KEY',    ':Z$}=h~z-[]8W}wZ(|/;]ZY;U{>u3K>P|u6/d:6}&h*);0ewhrcGm8$/tPii#{,%');
define('NONCE_KEY',        'd}Ue8.+KPDGdH^%2t~fTljDj8e{q{raR!Q0piqM(gcQP&7-$L3u0u|!Bn(-gh/?B');
define('AUTH_SALT',        'UQh.k>4UBTK6IecZe~lL6|J/w^KFROIeCdZw^g=(x?(L+j(-|%C FCt =V*XVz+k');
define('SECURE_AUTH_SALT', 'hHr&dDCApb14yz@ks+;}mk,s<-[]KUj*UAhl0+<pqT*!j;wxc+[mU[czh =><#em');
define('LOGGED_IN_SALT',   'WO,U^A||&:E@2A2_|MS W1l`fguj-7E{Fz6QkC+OM96Ho49<agM?O=G2FloxuF?6');
define('NONCE_SALT',       'uDN.1::MJ>~9Yp+e39y%o?r >:+OcUSqg5_g<Jt:Sr!um?U:U|MJ2q2qKiLDfmr@');

更新数据库配置到wp-config.php

更新url

开始使用http访问,后面切换到https后一直提示错误说跳转太多次。后来发现原因是套了cloudflare,cloudflare设置的是前端https后端http, 后端以http访问真实服务器后又跳转到https。所以请求一直在cf<->wordpress之间无限跳转。所以首先要设置cloudflare的代理模式为严格安全,strict full safe,也就是前端和后端都用证书。然后修改wordpress的url方法有很多,之前通过直接修改数据库,这次发现wordpress有个wp的命令行工具,一键修改。

vsftpd 配置默认目录

为local user配置默认目录,例如所有用户登录后进入/home目录, 添加配置local_root=/home,为匿名用户配置默认目录为tmp目录,则添加配置anon_root=/tmp

参考

https://serverfault.com/questions/544850/create-new-vsftpd-user-and-lock-to-specify-home-login-directory https://shiftedbits.org/2012/01/29/enable-wordpress-automatic-updates-on-a-debian-server/

目的

最近在公司开发一个amrnb/amrwb的转码工具,在网上看了很多资料,主要是这篇AMR在IP域中的编码(rfc3267,4867) 对我帮助很大。 这里补充一下缺少和我自己的理解。

AMR编码介绍

AMR是3GPP组织的一个音频编码规范,目前的手机都使用AMR, 比如公司的测试机华为、小米只支持amrnb/amrwb。amrnb是对窄带信号进行编码(200–3400 Hz),amrwb是对更宽的频率范围进行编码(50–7000 Hz)。 根据香农理论,采样频率要是目标频率的两倍,所以向上取整的amrnb的采样频率是8000Hz, amrwb的采样频率是16000Hz。

bitrate

amrnb和amrwb有多种编码速率(bitrate),并支持在通话过程中通过CMR(codec mode request)协商来变化编码速率,不同于一般的编码器固定一种比特率。除此之外,还有没有人声时的SID(舒适噪音)模式。这时只需要发送一个Mode而不是发送无声的包,从而来降低带宽占用。

amrnb 不同比特率时的语音帧大小

Index Mode Class A bits total speech bits
0 4.75 42 95
1 5.15 49 103
2 5.9 55 118
3 6.7 58 134
4 7.4 61 148
5 7.95 75 159
6 10.2 65 204
7 12.2 81 244

amrwb 不同比特率的语音帧大小

Index AMR-WB codec mode Total number of bits Class A Class B Class C
0 6.60 132 54 78 0
1 8.85 177 64 113 0
2 12.65 253 72 181 0
3 14.25 285 72 213 0
4 15.85 317 72 245 0
5 18.25 365 72 293 0
6 19.85 397 72 325 0
7 23.05 461 72 389 0
8 23.85 477 72 405 0

payload type

amrnb和amrwb使用的动态payload type, 所以在用wireshark分析时,需要经常调整payload-type值

align mode

amrnb和amrwb在SIP协商中有两种align mode,对齐模式和节省带快模式。总结有几个区别 * 对齐模式下CMR占8位,每个TOC占8位,然后每个语音帧补齐为整字节。 * 节省带宽模式下,CMR占4位,每个TOC占6位,然后CMR + TOC + 所有语音帧加起来,最后补齐为整字节。

以单帧的12.2kbps为例 * 语音帧为244位,对齐之后为(244 + 8 - 1)/8 = 31字节。加上1字节的CMR和1字节的TOC, 一共33字节。 * 语音帧为244位,加上4位CMR和6位TOC,一共为254位,对齐后为(254 + 8-1)/8,一种32字节字节。

以两帧的12.2kbps为例 * 两个语音帧分别占用31字节,加上1字节的CMR和2字节的TOC, 一共 65字节。 * 两个语音帧分别占用244位, 加上4位的CMR和6 * 2位的TOC, 对齐后为( 488 + 4 + 12 + 8 - 1)/8,一共63字节。

总之,对齐模式是先对齐成字节再求和,节省带宽模式是先计算位和再对齐成字节。特别是多帧的时候,节省带宽模式算起来更复杂。好在实际情况都是1帧。

基于opencore-amr的编解码

开始用FFmpeg的库来做转码和推流,后来发现不支持节省带宽模式,而且还有个BUG,所以后面自己基于opencore的库写的转码功能。

使用systemd-resolved的mdns

之前的博客中使用了avahi来广播本机的hostname和服务,这对于打印机和其他无屏设备非常重要。而较新版本Linux发行版中systemd-networkd+systemd-resolved同样能实现此功能

Network Manager

目前不少发行版默认的网络管理工具是NetworkManager, 需要先将其关闭和卸载,安装systemd-networkd包

配置systemd-networkd

networkd 中可以配置网桥,tun等设备,也可以直接在其中配置dns和dhcp等,在其中加入MulticastDNS=yes和Multicast=yes来启用mDns

[Match]
Name=eth0

[Network]
Address=192.168.4.42/24
DNS=114.114.114.114
Gataway=192.168.4.1
MulticastDNS=yes

[Link]
Multicast=yes

配置systemd-resolved

systemd-resolved功能类似与dnsmasq, 他会继承systemd-networkd中的dns信息,也会读取hostname用来广播,一般不用修改任何配置,之需要将其stub文件链接到/etc/resov.conf, 这样让系统的dns请求发到systemd-resolved

# ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
# ls -l /etc/resolv.conf
lrwxrwxrwx. 1 root root 37 May 22 22:03 /etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf

确认效果

通过resolvectl status命令可以查看resolve配置,如下全局的MulticastDNS是启用的,而networkd中的配置是per link的,也处于启用状态,Scopes里显示了mDNS,说明其启用了

      Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6 mDNS/IPv4 mDNS/IPv6
[root@netpanc ~]# resolvectl status
Global
       LLMNR setting: yes
MulticastDNS setting: yes
  DNSOverTLS setting: no
      DNSSEC setting: allow-downgrade
    DNSSEC supported: yes
          DNSSEC NTA: 10.in-addr.arpa
                      16.172.in-addr.arpa
                      168.192.in-addr.arpa
                      17.172.in-addr.arpa
                      18.172.in-addr.arpa
                      19.172.in-addr.arpa
                      20.172.in-addr.arpa
                      21.172.in-addr.arpa
                      22.172.in-addr.arpa
                      23.172.in-addr.arpa
                      24.172.in-addr.arpa
                      25.172.in-addr.arpa
                      26.172.in-addr.arpa
                      27.172.in-addr.arpa
                      28.172.in-addr.arpa
                      29.172.in-addr.arpa
                      30.172.in-addr.arpa
                      31.172.in-addr.arpa
                      corp
                      d.f.ip6.arpa
                      home
                      internal
                      intranet
                      lan
                      local
                      private
                      test

Link 2 (eth0)
      Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6 mDNS/IPv4 mDNS/IPv6
       LLMNR setting: yes
MulticastDNS setting: yes
  DNSOverTLS setting: no
      DNSSEC setting: allow-downgrade
    DNSSEC supported: yes
  Current DNS Server: 114.114.114.114
         DNS Servers: 114.114.114.114

在其他设备上ping可以成功

PING netpanc.local (192.168.4.43) 56(84) bytes of data.
64 bytes from netpanc.local (192.168.4.43): icmp_seq=1 ttl=64 time=0.350 ms
64 bytes from netpanc.local (192.168.4.43): icmp_seq=2 ttl=64 time=0.799 ms
64 bytes from netpanc.local (192.168.4.43): icmp_seq=3 ttl=64 time=0.733 ms

参考

https://wiki.archlinux.org/title/Systemd-resolved#mDNS

定义

c/cpp中,序列化是将基本类型或struct变为可存储和传递的形式,比如常用的socket send,将按协议组装的struct(pack)显示转换二进制(序列化)并发送给其他主机。而反序列化是将二进制转成基本类型或struct。另外还有fwrite/fread将结构体保存到文本中。 这种方式高效但容易出错。
而其他语言中可以使用xml, json等高级方式

转换方式

强制类型转换

上面讲到的两个函数send和fwrite,都是将基本类型或struct类型强转为字符串, 这种可能受不同机器的对齐影响

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

size_t fwrite(const void *ptr, size_t size, size_t nmemb,
    FILE *stream);

序列化

struct buffer {
  int a;
  char b;
  double c;
};

int main() {

  struct buffer buf;
  buf.a = 1;
  buf.b = 'a';
  buf.c = 2;

  FILE *f = fopen("/tmp/buf", "w+");
  if (f == NULL) {
    return 1;
  }
  fwrite(&buf, sizeof(struct buffer), 1, f);
}

反序列化

struct buffer {
  int a;
  char b;
  double c;
};

int main() {

  struct buffer buf;

  FILE *f = fopen("/tmp/buf", "r");
  if (f == NULL) {
    return 1;
  }

  fread(&buf, sizeof(struct buffer), 1, f);
  printf("%d--%c--%lf--\n", buf.a, buf.b, buf.c);
}

使用protobuf

syntax = "proto3";
package serialize;

option optimize_for = LITE_RUNTIME;

message Person {
    string name = 1;
    int32 id = 2;
}
tutorial::Person Serialize(std::string name, int id, std::string email){
    tutorial::Person person;
    person.set_name(name);
    person.set_id(id);

    return person;
}

void Deserialize(tutorial::Person person){
    std::cout << person.SerializeAsString();
}

optimize_for option

optimize_for (file option): Can be set to SPEED, CODE_SIZE, or LITE_RUNTIME. This affects the C++ and Java code generators (and possibly third-party generators) in the following ways:

  • SPEED (default): The protocol buffer compiler will generate code for serializing, parsing, and performing other common operations on your message types. This code is highly optimized.
  • CODE_SIZE: The protocol buffer compiler will generate minimal classes and will rely on shared, reflection-based code to implement serialialization, parsing, and various other operations. The generated code will thus be much smaller than with SPEED, but operations will be slower. Classes will still implement exactly the same public API as they do in SPEED mode. This mode is most useful in apps that contain a very large number of .proto files and do not need all of them to be blindingly fast.
  • LITE_RUNTIME: The protocol buffer compiler will generate classes that depend only on the "lite" runtime library (libprotobuf-lite instead of libprotobuf). The lite runtime is much smaller than the full library (around an order of magnitude smaller) but omits certain features like descriptors and reflection. This is particularly useful for apps running on constrained platforms like mobile phones. The compiler will still generate fast implementations of all methods as it does in SPEED mode. Generated classes will only implement the MessageLite interface in each language, which provides only a subset of the methods of the full Message interface.

其他还有如xml, json, ninja

背包问题

有一个承重为V的背包,有N个物品,重量分别为Cost[i], 价值分别为Value[i], 问选择放哪些物品时包的价值最大

状态方程

解决动态编程的关键是找到状态方程,如下。 解决背包问题就是比较不放哪个价值高

F [i, v] ← max{F [i − 1, v], F [i − 1, v − Ci] + Wi}

01背包

要求物品最大为1个, 这里有几个版本

#include <stdio.h>
#include <sys/param.h>

const int V = 4, N = 3;
const int cost[N] = {1, 3, 4};
const int value[N] = {15, 20, 30};

// const int V = 10, N = 5;
// const int cost[N] = {2, 3, 5, 2, 4};
// const int value[N] = {2, 2, 1, 1, 2};

void O1Package_N() {
  int dp[N][V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i <= V; i++) {
    dp[0][i] = 0;
  }

  // 初始化当包承重大于物品0重量的情况
  for (int j = cost[0]; j <= V; j++) {
    // 当只装物品0时,包的价值就是物品0的价值
    dp[0][j] = value[0];
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i < N; i++) {
    for (int v = 0; v <= V; v++) {
      // F [i, v] ← max{F [i − 1, v], F [i − 1, v − Ci] + Wi}
      // F[i][v] = MAX(F[i - 1][v], F[i - 1][v - cost[i]] + value[i]);
      int bufang = dp[i - 1][v];
      dp[i][v] = bufang;
      if (v >= cost[i]) {
        int fang = dp[i - 1][v - cost[i]] + value[i];
        dp[i][v] = MAX(bufang, fang);
      }
    }
  }

  for (int i = 0; i < N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N - 1][V]);
}

void O1Package_N_plus_1() {

  int dp[N + 1][V + 1];

  // 使用0个物品的价值为0
  for (int i = 0; i <= V; i++) {
    dp[0][i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i <= N; i++) {
    for (int v = 0; v <= V; v++) {
      dp[i][v] = dp[i - 1][v]; //当重量不够装i时,价值为装i-1的价值,
                               //此步骤以备i+1时使用(下一个i循环)
      if (v >= cost[i - 1]) {
        dp[i][v] = MAX(dp[i - 1][v], dp[i - 1][v - cost[i - 1]] + value[i - 1]);
      }
    }
  }

  for (int i = 0; i <= N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N][V]);
}

void O1Package_N_plus_1_origin() {

  int dp[N + 1][V + 1];

  // 使用0个物品的价值为0
  for (int i = 0; i <= V; i++) {
    dp[0][i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 0; i < N; i++) {
    for (int v = 0; v <= V; v++) {
      // 1. 当物品大于承重,物品不会放到背包,价值等于不放此物品时的情况
      // 即使物品小于承重,说明物品会放到背包。提前多赋值一次也是允许的
      dp[i + 1][v] = dp[i][v];
      if (v >= cost[i]) {
        dp[i + 1][v] = MAX(dp[i][v], dp[i][v - cost[i]] + value[i]);
      }
    }
  }

  for (int i = 0; i <= N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N][V]);
}

void O1Package_one_array_n() {

  int dp[V + 1];

  // 使用0个物品的价值为0
  for (int i = 0; i <= V; i++) {
    dp[i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = N - 1; i >= 0; i--) {
    for (int v = V; v >= cost[i]; v--) {
      int bufang = dp[v];
      int fang = dp[v - cost[i]] + value[i];
      dp[v] = MAX(bufang, fang);
    }
  }

  for (int j = 0; j <= V; j++) {
    printf("%d\t", dp[j]);
  }
  printf("\n");
  printf("%d\n", dp[V]);
}

完全背包

要求物品可以取无限个,但由于包的承重为V,所以每种物品最大数量为 V/cost[i]

void CompletePackage_use_n() {

  int dp[N][V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i < cost[0]; i++) {
    dp[0][i] = 0;
  }

  // 初始化当包承重大于物品0重量的情况
  for (int i = cost[0]; i <= V; i++) {
    // 当只装物品0时,包的价值就是物品0的价值
    dp[0][i] = i / cost[0] * value[0];
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i < N; i++) {
    for (int v = 0; v <= V; v++) {
      dp[i][v] = dp[i - 1][v]; //当重量不够装i时,价值为装i-1的价值,
                               //此步骤以备i+1时使用(下一个i循环)
      for (int k = 1; k <= v / cost[i]; k++) {
        dp[i][v] = MAX(dp[i - 1][v], dp[i - 1][v - k * cost[i]] + k * value[i]);
      }
    }
  }

  for (int i = 0; i < N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N - 1][V]);
}

void CompletePackage_use_n_plus_1() {

  int dp[N + 1][V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i <= V; i++) {
    dp[0][i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i <= N; i++) {
    for (int v = 0; v <= V; v++) {
      dp[i][v] = dp[i - 1][v]; //当重量不够装i时,价值为装i-1的价值,
                               //此步骤以备i+1时使用(下一个i循环)
      for (int k = 1; k <= v / cost[i-1]; k++) {
        dp[i][v] = MAX(dp[i - 1][v], dp[i - 1][v - k * cost[i-1]] + k * value[i-1]);
      }
    }
  }

  for (int i = 0; i <= N; i++) {
    for (int j = 0; j <= V; j++) {
      printf("%d\t", dp[i][j]);
    }
    printf("\n");
  }
  printf("%d\n", dp[N][V]);
}

void CompletePackage_use_one_array_no_reverse_V() {

  int dp[V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i <= V; i++) {
    dp[i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i <= N; i++) {
    for (int v = 0; v <= V; v++) {
      for (int k = 1; k <= v / cost[i-1]; k++) {
        dp[v] = MAX(dp[v], dp[v - k * cost[i-1]] + k * value[i-1]);
      }
    }
  }

  for (int j = 0; j <= V; j++) {
    printf("%d\t", dp[j]);
  }
  printf("\n");
  printf("%d\n", dp[V]);
}

void CompletePackage_use_one_array_reverse_V() {

  int dp[V + 1];

  // 0容量的价值为0, 合法
  for (int i = 0; i <= V; i++) {
    dp[i] = 0;
  }

  // 先遍历物品,后遍历重量
  for (int i = 1; i <= N; i++) {
    for (int v = V; v <= cost[i-1]; v--) {
      for (int k = 1; k <= v / cost[i-1]; k++) {
        dp[v] = MAX(dp[v], dp[v - k * cost[i-1]] + k * value[i-1]);
      }
    }
  }

  for (int j = 0; j <= V; j++) {
    printf("%d\t", dp[j]);
  }
  printf("\n");
  printf("%d\n", dp[V]);
}

多重背包

多重背包规定了每个物品的最大数量cap[i], 但由于要考虑背包承重,所以真正可以容纳物品的最大个数是max(cap[i], V/cost[i])

疑问

为何不需要将背包排序, 比如按重量排序或按价值排序,再将重量下而价值高的物品优先计算?

即使没有排序,由于第二层循环都是从0->V遍历和放物品i与不放物品i比较,所以顺序不重要

  for (int i = 1; i < N; i++) {
    for (int v = 0; v <= V; v++) {
      int bufang = dp[i - 1][v];
      dp[i][v] = bufang;
      if (v >= cost[i]) {
        int fang = dp[i - 1][v - cost[i]] + value[i];
        dp[i][v] = MAX(bufang, fang);
      }
    }
  }

算法复杂度和优化

矩阵数组的时间复杂度为O(VN),即两层for循环;空间复杂度为O(VN) 即申请一个矩形数组,经过一元数组优化后,空间复杂度变为O(V) 除此之外, 大多情况下,通过两层遍历,可以只保留重量相同且价值最大的那个物品,这个操作的时间复杂度为O(N^2), 但当最糟糕的情况,即精心设计的物品没有相同重量或者相同价值的情况则此优化无效 另外,先加物品重量大于背包重量的物品忽略,可以加快速度

理解优化

无论是用矩阵数组还是优化成一元数组,最好的理解方式是单步调试一遍

N行与N+1行

两种方式可以通用,只是当N行时,需要提前初始化物品0的情况

初始化问题

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了 F[0] 为 0,其它F[1..V ] 均设为 −∞,这样就可以保证最终得到的 F[V ] 是一种恰好装满背包的最优解。 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 F[0..V ]全部设为 0。

这是为什么呢?可以这样理解:初始化的 F 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什么也不装且价值为 0 的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为 -∞ 了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为 0,所以初始时状态的值也就全部为 0了。

贪心算法

由于背包问题里的物品具备重量价值两个维度,所以无法用贪心算法解决。而当维度为1的时候,可以用贪心算法,例如~零钱问题~和会议安排问题,因为不同零钱价值不同但其“重量”都为1,会议的“重量”也为1 : leetcode上的零钱问题不能用贪心算法

参考

《背包问题九讲》

FakeIt

FakeIt是一个单元测试工具,类比gmock, 但是比gmock更modern和轻量,因为它支持c++11和header-only

使用方法

FakeIt和gmock都提供Mock和调用计数功能,Mock通常用于接口类,用来定义并实例化接口类对象。gmock和FakeIt都能对调用参数进行校验,但FakeIt的Verify函数还提供在校验失败时打印参数值,这是gmock不具备的

struct IAppManager {
  // inotify calls when new event
  virtual void notify_add_event(EventType event_type,
                                const std::string& filepath) = 0;
  virtual void notify_rename_event(EventType event_type,
                                   const std::string& filepath,
                                   const std::string& filepath_new) = 0;

  virtual ~IAppManager() = default;
};

TEST_CASE("Test Inotify") {
  Mock<IAppManager> appmanager_mock;
  Fake(Dtor(appmanager_mock));
  Fake(Method(appmanager_mock, notify_add_event));
  Fake(Method(appmanager_mock, notify_rename_event));

  auto& app_manager = appmanager_mock.get();
  SECTION("test add") {
    std::thread t1([&app_manager](){
      app_manager.notify_add_event(EventType::File, "filename");
      app_manager.notify_add_event(EventType::File, "filename");
      app_manager.notify_add_event(EventType::File, "filename");
    });
    t1.join();
    Verify(Method(appmanager_mock, notify_add_event)).Exactly(2);
  }
}

这里没有对调用参数校验,而仅对调用次数验证,可以预见,结果失败,因为调用了3次。

问题

上面的测试用例会失败,因为次数不匹配。而这时FakeIt会打印所有调用的参数值,但参数由于是个引用,而且引用在线程退出时被释放,打印一个无效引用导致会抛异常

due to a fatal error condition:                                                                 
  SIGSEGV - Segmentation violation signal 

解决办法

对于本测试而言,解决办法就是将传参所引用的对象duration改大,如全局变量或者static,或者在线程外定义它

TEST_CASE("Test Inotify") {
  Mock<IAppManager> appmanager_mock;
  Fake(Dtor(appmanager_mock));
  Fake(Method(appmanager_mock, notify_add_event));
  Fake(Method(appmanager_mock, notify_rename_event));

  auto& app_manager = appmanager_mock.get();
  SECTION("test add") {
    std::string filename = "asd";
    std::thread t1([&app_manager, &filename](){
      app_manager.notify_add_event(EventType::File, filename);
      app_manager.notify_add_event(EventType::File, filename);
      app_manager.notify_add_event(EventType::File, filename);
    });
    t1.join();
    Verify(Method(appmanager_mock, notify_add_event)).Exactly(2);
  }
}
修改之后,不会触发SIGSEGV异常, 而是能正确打印出调用参数

  Expected pattern: appmanager_mock.notify_add_event( Any arguments )
  Expected matches: exactly 2
  Actual matches  : 3
  Actual sequence : total of 3 actual invocations:                             
    appmanager_mock.notify_add_event(?, asd)
    appmanager_mock.notify_add_event(?, asd)
    appmanager_mock.notify_add_event(?, asd) 

但对于间接调用的接口,且在单元测试里无法控制变量类型,则没有什么办法,只能保证Extractly准确而不用打印参数值

gmock的方式

gmock使用预先设定调用参数,在调用时再判断,所以不会出现无效引用的问题

print(stringify) variable of different types

typeof is a GCC extension, and _Generic is part of C11. typeof is disabled by option '-std=*' or '-ansi, so usetypeof` instead.

c11 _Generic

#include <stdio.h>

#define PRINT_VAR(var)        \
    _Generic((var),           \
        int: print_int,       \
        float: print_float,   \
        double: print_double, \
        char: print_char      \
    )(#var, (var))

// Type-specific print functions
void print_int(const char* name, int value) {
    printf("%s = %d\n", name, value);
}

void print_float(const char* name, float value) {
    printf("%s = %.2f\n", name, value);
}

void print_double(const char* name, double value) {
    printf("%s = %.2f\n", name, value);
}

void print_char(const char* name, char value) {
    printf("%s = %c\n", name, value);
}

int main() {
    int x = 10;
    float y = 3.14f;
    double z = 3.14159;
    char c = 'A';

    PRINT_VAR(x);  // Prints: x = 10
    PRINT_VAR(y);  // Prints: y = 3.14
    PRINT_VAR(z);  // Prints: z = 3.14
    PRINT_VAR(c);  // Prints: c = A

    return 0;
}

simpler way

linter raise warnings if place printf directly in _Generic.

#define DECLARE_PRINT_FUNC(value)                                              \
  do {                                                                         \
    printf("%s = ", #value);                                                   \
    char *format = NULL;                                                       \
    _Generic((value),                                                          \
        int: format = "%d\n",                                                  \
        long unsigned int: format = "%lu\n",                                   \
        long: format = "%ld\n",                                                \
        float: format = "%.2f\n",                                              \
        double: format = "%.2f\n",                                             \
        char: format = "%c\n",                                                 \
        default: format = NULL);                                               \
    format != NULL ? printf(format, (value)):                                  \
    printf("Unknown type\n");                                                  \
  } while (0)

using GCC build-in typeof instead of _Generic

#include <stdio.h>

#define PRINT_VAR_TYPEOF(var)                                                  \
  do {                                                                         \
    printf("%s = ", #var);                                                     \
    char *format = NULL;                                                       \
    __typeof__(var) _v = (var);                                                \
    if (__builtin_types_compatible_p(__typeof__(_v), int))                     \
      format = "%d\n";                                                         \
    else if (__builtin_types_compatible_p(__typeof__(_v), float))              \
      format = "%.2f\n";                                                       \
    else if (__builtin_types_compatible_p(__typeof__(_v), double))             \
      format = "%.2f\n";                                                       \
    else if (__builtin_types_compatible_p(__typeof__(_v), char))               \
      format = "%c\n";                                                         \
    printf(format, (var));                                                     \
  } while (0)

三指针法翻转单向链表

定义3个指针, 原理是三个指针分别指向前三个元素,将第一个元素指向第二个元素,然后全部后移 1. 第三个指针向前移动 2. 将第二个元素指向第一个元素 3. 第一个指针和第二个指针向前移动

struct Node{
  int data;
  Node *next;
};

Node *reverse(Node *head) {
  Node *pPrev = nullptr;
  Node *pHead = head;
  Node *pNext = nullptr;
  while (pHead) {
    pNext = pHead->next;
    pHead->next = pPrev;
    pPrev = pHead;
    pHead = pNext;
  }
  return pHead;
}
Node *reverse(Node *head) {
  if (!head) {
    return head;
  }
  Node *first = nullptr;
  Node *second = head;
  Node *third = head->next;

  while (third) {
    second->next = first;

    first = second;
    second = third;
    third = third->next;
  }

  second->next = first;

  return second;
}

翻转链表的部分节点

翻转node[m]->node[N]部分, 理解时先将链表分为3部分: part1->part2->part3, 只翻转part2

翻转动作套用三指针法,但部分翻转的难点在于处理part1为空和非空的情况,所以要判断m时候为1 * m==1即要翻转第一个元素时,需要先记录part2翻转后的tail node,最后将其next指向part3的第一个元素 * m!=1即part1不为空,需要先记录part2翻转后的tail node和part1的最后一个node,最后将其指向part2的新head,以及将part2的第一个node指向part3

struct ListNode *reverseBetween(struct ListNode *head, int m, int n) {
  if (m == n) {
    return head;
  }

  if(!head || !head->next){
    return head;
  }

  // write code here
  struct ListNode *pleft = NULL;
  struct ListNode *phead = head;
  struct ListNode *pright = NULL;


  // 只有当m!=1时才需要,指向part1的最后一个元素
  struct ListNode *pPart1Tail = NULL;
  // 当m=1时指向part2的第一个元素
  struct ListNode *pPart2NewTail = NULL;

  // skip m
  for (int i = 1; i < m; i++) {
    pleft = phead; 
    phead = phead->next;
  }

  // 保存part2的head,也是翻转后的tail
  pPart2NewTail = phead;
  // m != 1 时pleft不为NULL
  struct ListNode *pPartOneTail = pleft;

  int i = 0;
  // 翻转次数要比m-n大1
  while (phead && i < n - m + 1) {
    pright = phead->next;

    phead->next = pleft;

    pleft = phead;
    phead = pright;
    i++;
  }

  if(m==1){
    // 指向part3的头
    pPart2NewTail->next = phead;
    return pleft;
  }

  pPartOneTail->next = pleft;
  pPart2NewTail->next = phead;

  return head;
}

在Linux下使用sdcc进行单片机开发

周末研究了下单片机,本来准备写个定时器来控制电扇开关,发现比起树梅派,单片机实在不太方便

开发环境搭建

Windows大家都用Keil, 一站式比较方便,但这是个收费软件。而在Linux下,使用到一些组合来实现编译和烧录

  1. sdcc 开源的编译器,类似gcc
  2. stcflash github上的开发者开发的烧录脚本

sdcc 在Debian有包可以直接安装

# apt install sdcc
再 clone github的代码到本地,需要使用root权限执行烧录。而我执行是发现缺少python serial包,要全局安装就使用apt命令

# apt install python3-serial

这样环境就搭建好了

编写代码

用c语言开发时要引入比较的定义头文件,我的单片机是好多年前买的国产stc 89C52RC, 是基于51的拓展版本,于是用了sdcc的8052.h头,路径在

// /usr/share/sdcc/include/mcs51/8052.h
#ifndef REG8052_H
#define REG8052_H

#include <8051.h>     /* load definitions for the 8051 core */

#ifdef REG8051_H
#undef REG8051_H
#endif

不过不需要我们指定路径,sdcc会自动引入。窥视其内容可以发现它包含了8051的东西,所以说52是基于51的

回顾一下51单片机知识

  1. 单片机的寄存器初始是0, 而引脚初始都是高点位
  2. 定时器和计数器的触发事件都是靠中断,中断执行当前(main)中的函数而执行中断响应函数,这跟Unix内核是一样的。 单片机还具有多个优先级的中断,可以嵌套
  3. 中断的触发靠计数寄存器(TLx和THx)溢出,这两个都是8位寄存器,所以同时使用时,在2^16时溢出。但也有只用其中一个寄存器的时候,这由TMOD寄存器控制

TMOD寄存器

TMOD是不可按位访问的寄存器,意思是只能一次性赋值所有位。 TMOD控制着两个定时器/计数器0和1

timer.png

  • TMOD高四位用于设置定时器/计数器1,低四位用于设置定时器/计数器0;
  • GATE寄存器决定是否使用外部INTx控制,即软件自动启动,还是外部启动,这里赋值0
  • GATE=1时,“与门”的输出信号K由INTx输入电平和TRx位的状态一起决定(即此时K=TRx·INTx),当且仅当TRx=1,INTx=1(高电平)时,计数启动;否则,计数停止。当INT0引脚为高平时且TR0置位,TR0=1;启动定时器T0
  • GATE=0时,“A”输出恒为1,“B”的值由TRx决定,当TR0=1,启动定时器/计数器T0,当TR1=1,启动定时器/计数器T1
  • CT位为1时选择计数器模式,为0时选择定时器模式
  • M1,M0用于选择工作方式,当M0=1,M1=0时,使用TL0和TH0的16位来计数

TCON寄存器

TCON.png

定时器初始化

这里使用定时器0

void Timer0Init(void) // 1毫秒@12.000MHz
{
    TMOD = 0x01; //设置定时器模式 0000 0001
    // TCON
    TF0 = 0; //初始化溢出标志
    TR0 = 1; //定时器0开始计时
    // 计数器初始值,再触发一次就溢出了
    TH0 = 0xFF; //设置定时初值
    TL0 = 0xFE; //设置定时初值
    // 中断通路
    EA = 1; 
    ET0 = 1;
    PT0 = 0;
}

计数器模式

这里使用计数器0

void Count0Init(void) // 1毫秒@12.000MHz
{ 
    TMOD = 0x05; //设置计数器模式 0000 0101

    // TCON
    TF0 = 0; 
    TR0 = 1; //定时器0开始计时

    // 计数器初始值,再触发一次就溢出了
    TH0 = 0xFF;  //设置定时初值
    TL0 = 0xFE;  //设置定时初值

    // 中断通路
    EA = 1;
    ET0 = 1;
    PT0 = 0;
}

中断响应函数

sdcc和keil有一些区别,中断标志符使用__interrupt

static unsigned int T0Count = 0;
void timer0_routine() __interrupt (1) {
  T0Count++;
  TH0 = 0xFF; //设置定时初值
  TL0 = 0xFE; //设置定时初值
  if (T0Count == 3) {
    T0Count = 0;
    P1_1 = 0;  // 0输出低电平,灯泡亮
  }
}

完整代码

// main.c
#include "8052.h"

void Timer0Init(void) // 1毫秒@12.000MHz
{
  TMOD = 0x01;
  TF0 = 0;
  TR0 = 1;
  TH0 = 0xFF;
  TL0 = 0xFE;
  EA = 1;
  ET0 = 1;
  PT0 = 0;
}

void main(void) {

  Timer1Init();
  while (1)
    ;
}

unsigned int T0Count = 0;
void timer0_routine() __interrupt(1) {
  T0Count++;
  TH0 = 0xFF;
  TL0 = 0xFE;
  if (T0Count == 3) {
    T0Count = 0;
    P1_1 = 0;
  }
}

编译和烧录

代码保存为main.c

# 使用sdcc编译
sdcc main.c
将sdcc生成的ihx文件转为hex文件
packihx main.ihx  > main.hex   

烧录到单片机

sudo python ~/github/stcflash/stcflash.py main.hex
Connect to /dev/ttyUSB0 at baudrate 2400
Detecting target... done
 FOSC: 11.955MHz
 Model: STC89C52RC (ver4.3C) 
 ROM: 8KB
 [X] Reset stops watchdog
 [X] Internal XRAM
 [X] Normal ALE pin
 [X] Full gain oscillator
 [X] Not erase data EEPROM
 [X] Download regardless of P1
 [X] 12T mode
Baudrate: 38400
Erasing target... done
Size of the binary: 147
Programming: #################### done
Setting options... done