跳转至

Welcome

ibus-setup

这里记录下ibus的配置记录,重新安装了系统,默认安装的ibus。之前用的fcitx5还不错,不过都尝试下。遇到两个问题:

firefox里面单击选中的文字触发删除

之前安装的ibus-pinyin, 重新安装ibus-libpinyin后解决。参考

配置默认输入法

作为一名程序员,默认英文输入是基本素质,在fcitx5中强制的英文输入法是默认输入法。但ibus中不是,但可以通过dbus来修改。打开dconf-editor,依次找到dconf-editor -> desktop -> ibus -> general,直接修改preload-engines的用户定义顺序,engines-order会跟随变化。将ibus:us:en提到ibus-pinyin数组前面就可以了。参考

ibus 体验

ibus中的好像输入表情要容易些。

gnome 配置ibus

最近切换到gnome-wayland, 配置ibus花了挺多时间。问题跟网上说的什么locale无关,gnome中的locale文件是修改配置时自动更新的。最终无头无脑卸载了fcitx相关的东西,并且重装了ibus和ibus-libpinyin,然后可能是重启后成功。

libavcodec使用

记录ffmpeg的学习系列,这是关于AVCodecContext的部分。

sample_fmt

解码器avctx中的sample_fmt,是解码后的原始音频(PCM)采样格式,几乎所有的音频格式(amr,mp3)解码后都是PCM。但是采样格式可能不同,这个是由解码器来设定的。 具体在打开解码器的时候avcodec_open2, 需要传入解码解码avctx, 这个函数会调用解码器的init函数,从而将解码器的设定同步到avctx中。 对于编码器而言,这个值由是用户自己设置。如注释所说:

    /**
     * audio sample format
     * - encoding: Set by user.
     * - decoding: Set by libavcodec.
     */
    enum AVSampleFormat sample_fmt;  ///< sample format

所有的采样格式。因为工作需要解码amr, 先是自己基于opencore开发的,解码后是S16(即AV_SAMPLE_FMT_S16), 而用ffmpeg解码后是AV_SAMPLE_FMT_FLTP, 这样的话重采样一次,效率低。后来发现ffmpeg有两套amr解码实现,选择opencore版本就是S16了。

enum AVSampleFormat {
    AV_SAMPLE_FMT_NONE = -1,
    AV_SAMPLE_FMT_U8,          ///< unsigned 8 bits
    AV_SAMPLE_FMT_S16,         ///< signed 16 bits
    AV_SAMPLE_FMT_S32,         ///< signed 32 bits
    AV_SAMPLE_FMT_FLT,         ///< float
    AV_SAMPLE_FMT_DBL,         ///< double

    AV_SAMPLE_FMT_U8P,         ///< unsigned 8 bits, planar
    AV_SAMPLE_FMT_S16P,        ///< signed 16 bits, planar
    AV_SAMPLE_FMT_S32P,        ///< signed 32 bits, planar
    AV_SAMPLE_FMT_FLTP,        ///< float, planar
    AV_SAMPLE_FMT_DBLP,        ///< double, planar
    AV_SAMPLE_FMT_S64,         ///< signed 64 bits
    AV_SAMPLE_FMT_S64P,        ///< signed 64 bits, planar

    AV_SAMPLE_FMT_NB           ///< Number of sample formats. DO NOT USE if linking dynamically
};

docker network

记录一次docker的网络问题,以及学到的知识。

背景

公司用docker容器替代虚拟机来隔离,用来ssh登录以及运行不同的服务。容器连接了双网络,一个用来访问同宿主机上的容器,另一个用来从其他主机访问。

问题

宿主机重启之后,容器无法访问。需要手动改路由。

解决办法

每次修改路由麻烦,后面发现两个网络都是macvlan时,存在无法没有网络。

创建桥接物理网卡

这是需要指定parent为物理网卡,既然是桥接物理网卡,就能从其他宿主机访问了。

docker network create -d bridge   --subnet=10.9.3.0/24   --gateway=10.9.3.1   -o parent=ens39f0 public_net_bridge

docker network create -d macvlan  --subnet=10.9.3.0/24   --gateway=10.9.3.1   -o parent=ens39f0 public_net_macvlan
这两个由于使用的同一个网段/网卡,所以不可能同时存在。也许可以通过指定不同的ip-range来实现吗?

创建桥接虚拟网卡

这种网络就不能从非宿主机的其他主机访问,只能在同宿机下的容器之间访问。我称之为私有网络

docker network create -d bridge   private_net_bridge

docker network create -d macvlan  private_net_macvlan
这里没有指定网络地址

iperf测速

发现无论桥接物理网卡还是虚拟网卡,macvlan都要比bridge快

  • 私有网络 bridge 模式分别测得: 20.4 Gbits/sec 22.6 Gbits/sec 23.0 Gbits/sec macvlan 模式分别测得: 26.1 Gbits/sec 24.4 Gbits/sec 26.7 Gbits/sec

  • 桥接物理网卡 macvlan: 25.7 Gbits/sec 25.5 Gbits/sec 27.3 Gbits/sec bridge: 20.2 Gbits/sec 20.4 Gbits/sec 23.2 Gbits/sec

顺便说下容器连接多网络的方法

容器启动的时候,只能指定一个网络, 然后启动之后,可以通过命令来连接多网络,可以在启动时设置--restart=alway,这样重启主机后自动连接双网络

docker network connect private_net_bridge test_container

杂项

网络配置挺复杂,macvlan 下面还分 * Bridge mode, 就是常见的模式,走宿主机的网卡 * 802.1q trunk bridge mode, 先在物理网卡上创建子网卡

IPvlan

除了bridge macvlan之外,还有IPvlan, 这种模式下,所有的容器都使用相同的mac地址,所以如果容器使用dhcp来获取ip地址,则可能有问题。 访问其他容器时,可以配置来使用macvlan模式。

overlay

docker network driver 除了上面的三种模式之外,还有overlay, 用于连接不同主机的网络,可以跨宿主机访问容器。

宿主机网络类型

我通常会创建bridge并将物理网卡设为从卡,然后创建虚拟网桥,用于访问虚拟机。而除了bridge, 还有bond,另外还有team。总而言之,bridge可以使不同的虚拟机(client)使用网卡的虚拟化功能,而bond可以将不同的网卡绑定,实现负载均衡。如果两个网卡分别有1M带宽,那么bond后的网卡则有2M带宽。下面的链接先bonding,然后在bonding网卡上创建bridge网卡。参考

Linux下的邮件配置

这里记录Linux(debian)下如何使用Mutt查看邮件,用fetchmail接收邮件,用msmtp发送邮件以及使用maildrop来筛选邮件。与其他邮件客户端相比,如thunderbird,他们使用的在线登陆方式,没有下载邮件来离线访问。而mutt支持离线访问本地的邮件,也可以配置为访问邮件服务器,查看服务器上的邮件。 在线方式除了离线不可用的缺点之外,每次打开mutt需要登陆,非常慢。 我这里的配置是比较简陋,适合公司少量邮件的场景。又因为目前使用的Byobu-terminal终端,其邮件提醒是基于/var/mail/spool/$USER文件。邮件比较多或者附件比较大的话,相较于用文件夹来存储邮件,这种方式会比较慢。

软件安装

需要安装 fetchmail, maildrop, mutt

接收配置

在家目录下创建文件.fetchmailrc

poll imap.exmail.qq.com
  protocol IMAP
  user "example@example.com"
  password "password"
  keep
  #fetchall
  ssl

mimedecode
mda "/usr/bin/maildrop"
keep 方式不会删除服务器上的邮件。然后启动定时任务,自动检查新邮件
systemctl --user enable --now fetchmail.service

service 内容

# /usr/lib/systemd/user/fetchmail.service
# or any other location per man:systemd.unit(5)
[Unit]
Description=Fetchmail Daemon
Documentation=man:fetchmail(1)

[Service]
ExecStart=fetchmail --nodetach --daemon 300
ExecStop=fetchmail --quit
Restart=always

[Install]
WantedBy=default.target

发送配置

在家目录创建文件.msmtproc,填入发送的验证信息

defaults
auth           on
tls            on
tls_starttls off
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        ~/.msmtp.log

# exmail
account        office
host           smtp.exmail.qq.com
port           465
from           example@example.com
user           example@example.com
password       password

mutt配置

由于我不想删除/var/mail/里面的邮件,这样才能在byobu-terminal中提醒,所以没有配置maildrop。最后需要配置mutt读取/var/mail/$USER。在家目录或者xdg配置目录(~/.config/mutt)创建文件.muttrc/muttrc

set spoolfile=/var/spool/mail/jimery
set header_cache=~/.cache/mutt
set send_charset="utf-8"

这样就能查看邮件了。 在mutt里面手动管理邮件,存档或者删除。

mutt使用

其实不用mutt, 直接用mail也可以,这个默认访问/var/mail/$USER,是Linux默认的邮件工具。 mutt能很方便调用msmtproc来发送邮件。

maildrop in Arch

又换回Arch了,发现maildrop在AUR里,而且还依赖好几个AUR, 而自己编译只依赖courier-unicode库。

  1. 下载courier-unicode源码
  2. ./configure --prefix=$HOME/.local --enable-shared=no --with-pic # 静态编译,指定安装到用户根目录
  3. make && make install # 必须安装否则编译maildrop的时候找不到这个库,即使指定路径也不行。
  4. 下载maildrop源码
  5. ./configure --prefix=/home/hst/.local --enable-shared=no # 静态编译,指定安装到用户根目录
  6. make
  7. 验证是静态链接的courier-unicode,确认ldd ./libs/maildrop/maildrop 里面没有'courier-unicode'

Arch里面创建用户时默认没有对应的邮箱文件,直接手动创建sudo touch "/var/spool/mail/$(whoami)", 还有chmod

安装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上的零钱问题不能用贪心算法

参考

《背包问题九讲》