线性规划对偶问题 学习笔记
线性规划
线性规划是一类满足限制条件为关于自变量的线性约束,且目标函数是关于自变量的线性函数的一类最优化问题。
对于一组自变量 \(x_1,x_2,\dots,x_n\),定义线性函数 \(f(x_1,x_2,\dots,x_n)=\sum_{i=1}^{n}c_ix_i\)。不等式 \(f(x_1,x_2,\dots x_n)\le b,f(x_1,x_2,\dots x_n)\ge b\) 以及等式 \(f(x_1,x_2,\dots x_n=b\) 即被称为线性约束。
经过变换,所有线性规划都可以被写作标准型:
具体的,\(\ge\) 的线性约束可以将系数取反后变为 \(\le\),\(=\) 可以拆为一个 \(\ge\) 和 \(\le\)。如果对变量 \(x_i\) 没有 \(\ge 0\) 的约束,可以加入新变量 \(x_1,x_2\) 满足 \(x_i=x_1-x_2\),令$ x_1,x_2\ge 0$,与原问题等价。
这个式子可以用矩阵表示:
其中定义向量 \(x\le y\) 当且仅当 \(x\) 的每一维都 \(\le y\)。
举几个例子:
-
最大流问题转化为线性规划:
定义 \(f_{u,v}\) 为边 \((u,v)\) 的流量,\(c_{u,v}\) 为边 \((u,v)\) 的容量,对于源汇点的问题,从汇点向源点连接一条流量为 \(\inf\) 的边,使得每个点都满足流量守恒,记 \(V,E\) 分别为点集、边集。则最大流问题相当于:
\[\max f_{t,s}\\ s.t.\begin{cases} f_{u,v}\le c_{u,v}&(u,v)\in E\\ \sum_v f_{u,v}=\sum_v f_{v,u}&u\in V\\ f_{u,v}\ge 0&(u,v)\in E\cup\{(t,s)\} \end{cases} \] -
最小费用流问题:即在满足流量守恒的条件下使费用最小,不需要满足流量最大的限制,记 \(w_{u,v}\) 为边 \((u,v)\) 的费用。
\[\max \sum_{(u,v)\in E}f_{u,v}w_{u,v}\\ s.t.\begin{cases} f_{u,v}\le c_{u,v}&(u,v)\in E\\ \sum_v f_{u,v}=\sum_v f_{v,u}&u\in V/\{s,t\}\\ f_{u,v}\ge 0&(u,v)\in E \end{cases} \]
对偶问题
形式化定义:对于标准型线性规划
定义它的对偶线性规划为:
其中 \(y\) 称为对偶变量。可以看出者相当于将每条线性约束变换为了一个对偶变量,目标线性函数则变成了约束。
写成矩阵的形式:
对偶后:
-
弱对偶定理:
即对于上述两个线性规划,有 \(c^T x\le b^T y\)。
考虑由于 \(y\) 满足对偶线性规划,故 \(\sum_{i=1}^{m}a_{i,j}y_i\ge c_j\)。
故
\[\sum_{j=1}^{n}c_jx_j\le \sum_{j=1}^{n}(\sum_{i=1}^{m}a_{i,j})x_jy_i=\sum_{i=1}^{m}(\sum_{j=1}^{n}a_{i,j}x_j)y_i\le \sum_{i=1}^{m}b_iy_i \] -
强对偶定理:
即对于上述两个线性规划,有 \(c^T x= b^T y\)。
证明较为复杂,在此不多赘述。
由此可以看出,线性规划与其对偶问题的最优解相同,可以求其对偶问题的最优解得到原问题的最优解。
经典问题的对偶
-
最大流问题:原线性规划:
\[\max f_{t,s}\\ s.t.\begin{cases} f_{u,v}\le c_{u,v}&(u,v)\in E\\ \sum_v f_{u,v}-\sum_v f_{v,u}=0&u\in V\\ f_{u,v}\ge 0&(u,v)\in E\cup\{(t,s)\} \end{cases} \]这个线性规划有两种线性约束。记 \(f_{u,v}\le c_{u,v}\) 的对偶变量为 \(d_{u,v}\),\(\sum_v f_{u,v}-\sum_v f_{v,u}=0\) 产生的对偶变量为 \(p_u\)。
故对偶后,目标函数中 \(p_u\) 的系数抵消为 \(0\),对偶问题即为:
\[\min \sum_{(u,v)\in E}c_{u,v}d_{u,v}\\ s.t.\begin{cases} d_{u,v}-p_u+p_v\ge 0&(u,v)\in E\\ p_s-p_t\ge 1\\ p_u\ge 0&u \in V\\ d_{u,v}\ge 0&(u,v)\in E \end{cases} \]考虑最小割问题:设一个割将所有点分为两个不相交集合 \(S,T\),令 \(p_u=[u\in S]\),\(d_{u,v}\) 表示边 \((u,v)\) 是否被割去,写出线性规划为:
\[\min \sum_{(u,v)\in E}c_{u,v}d_{u,v}\\ s.t.\begin{cases} d_{u,v}-p_u+p_v\ge 0&(u,v)\in E\\ p_s-p_t\ge 1\\ p_u\in[0,1]&u \in V\\ d_{u,v}\in[0,1]&(u,v)\in E \end{cases} \]可以看出与最大流对偶问题的唯一区别在于限制条件 \(p_u,d_{u,v}\in[0,1]\)。不过可以证明一定存在一组 \(\in[0,1]\) 的最优解满足这一限制,因此我们就得到了经典结论:最大流等于最小割。
-
最小费用流问题:为了应对更广泛的问题,我们设 \(b_u\) 表示点 \(u\) 要求流出流量减流入流量 \(=b_u\),故:
\[\min \sum_{(u,v)\in E}f_{u,v}w_{u,v}\\ s.t.\begin{cases} -f_{u,v}\ge -c_{u,v}&(u,v)\in E\\ \sum_v f_{v,u}-\sum_v f_{u,v}=-b_u&u\in V\\ f_{u,v}\ge 0&(u,v)\in E \end{cases} \]令 \(z_{u,v}\) 为 \(f_{u,v}\le c_{u,v}\) 的对偶变量,\(p_u\) 为 \(\sum_v f_{u,v}-\sum_v f_{v,u}=b_u\) 的对偶变量,则对偶问题为:
\[\max \sum_u-b_up_u-\sum_{(u,v)\in E}z_{u,v}c_{u,v}\\ s.t.\begin{cases} p_v-p_u-z_{u,v}\le w_{u,v}&(u,v)\in E\\ z_{u,v}\ge 0&(u,v)\in E\\ p_u\ge 0&u\in V \end{cases} \]将所有 \(p_v-p_u-z_{u,v}\le w_{u,v}\) 的限制加起来,并将所求函数取负后相加,于是答案就是下面这个式子的相反数:
\[\min \sum_ub_up_u+\sum_{(u,v)\in E}c_{u,v}\max(p_v-p_u-w_{u,v},0) \]因此形如这样式子的问题,可以将其转化为最小费用流问题求解。
还有一件事:对于下面这个问题,我们一般还会要求 \(p\) 均为整数。不过可以用调整法证明,如果所有 \(w_{u,v}\) 均为整数,一定可以在所有 \(p_u\) 均为整数时取到最优解。
例题
-
对于题目中的区间限制 \((l,r,d)\),可以想到记前缀和数组 \(p\),那么该限制相当于 \(p_r-p_{l-1}-d\ge 0\),同时也要满足 \(p_i-p_{i-1}\ge 0\),最终的费用就是 \(\sum_i (c_i-c_{i+1})p_i\)。
这几个限制都可以通过乘上 \(+\infty\) 的的系数来体现,于是相当于要求:
\[\min \sum_u(c_u-c_{u+1})p_u+\left(\sum_{(l,r,d)}+\infty\max(p_{l-1}+d-p_r,0)\right)+\left(\sum_{i}+\infty\max(p_{i-1}-p_i,0)\right) \]的相反数。
这就是上面最小费用流的柿子。于是对于 \(u\in[0,n]\),如果 \(c_u-c_{u+1}>0\) 就从源点向 \(u\) 连边 \((c_u-c_{u+1},0)\),否则从 \(u\) 向汇点连边 \((c_{u+1}-c_u,0)\)。从 \(u\) 向 \(u-1\) 连边 \((+\infty,0)\),从 \(r\) 向 \(l-1\) 连边 \((+\infty,-d)\),跑出最小费用最大流的相反数即为答案。
直接跑 \(SPFA\) 会 \(TLE\),需要用 \(johnson\) 算法先转化为正权边,再跑 \(dijkstra\)。
#include<bits/stdc++.h> using namespace std; const int M=5e4+10,N=5e3+10,inf=0x3f3f3f3f; namespace mcmf{ int tot,s,t,first[N],cnt=1,dis[N],vis[N],cur[N],h[N],rec[N]; struct node{ int u,v,f,c,nxt; }e[M*2]; inline void add(int u,int v,int f,int c){ e[++cnt].u=u;e[cnt].v=v;e[cnt].f=f;e[cnt].c=c; e[cnt].nxt=first[u]; first[u]=cnt; } inline void Add(int u,int v,int f,int c){ add(u,v,f,c); add(v,u,0,-c); } inline bool SPFA(){ queue<int>q; memset(dis+1,0x3f,sizeof(int)*(tot)); dis[s]=0;vis[s]=1; q.push(s); while(!q.empty()){ int u=q.front();q.pop();vis[u]=0; for(int i=first[u];i;i=e[i].nxt){ int v=e[i].v; if(e[i].f>0&&dis[v]>dis[u]+e[i].c){ dis[v]=dis[u]+e[i].c; if(!vis[v]){vis[v]=1;q.push(v);} } } } for(int i=1;i<=tot;++i) rec[i]=dis[i]; return dis[t]<inf; } inline bool dijkstra(){ for(int i=1;i<=tot;++i) h[i]+=rec[i]; priority_queue<pair<int,int> >q; memset(dis+1,0x3f,sizeof(int)*(tot)); memset(vis+1,0,sizeof(int)*(tot)); dis[s]=0; q.push(make_pair(0,s)); while(!q.empty()){ int u=q.top().second;q.pop(); if(vis[u]) continue;vis[u]=1; for(int i=first[u];i;i=e[i].nxt){ int v=e[i].v,tmp=h[u]+e[i].c-h[v]; if(e[i].f>0&&dis[v]>dis[u]+tmp){ dis[v]=dis[u]+tmp; q.push(make_pair(-dis[v],v)); } } } for(int i=1;i<=tot;++i) rec[i]=dis[i]; return dis[t]<inf; } inline int dfs(int u,int f){ if(u==t||!f) return f; int used=0;vis[u]=1; for(int& i=cur[u];i;i=e[i].nxt){ int v=e[i].v; if(vis[v]||dis[v]!=dis[u]+(h[u]+e[i].c-h[v])) continue; int fl=dfs(v,min(f,e[i].f)); e[i].f-=fl;e[i^1].f+=fl; used+=fl;f-=fl; if(!f) break; } if(f) dis[u]=0x3f3f3f3f; return vis[u]=0,used; } inline int dinic(int S,int T){ s=S;t=T; int mxf=0,mxc=0; for(SPFA();dijkstra();){ memset(vis+1,0,sizeof(int)*(tot)); memcpy(cur,first,sizeof(int)*(tot)); int flow=dfs(s,inf); mxf+=flow;mxc+=(dis[t]-h[s]+h[t])*flow; } return mxc; } } using mcmf::tot; using mcmf::Add; int n,m,c[N],d[N]; int main(){ scanf("%d%d",&n,&m); tot=n+1; int s=++tot,t=++tot; for(int i=1;i<=n;++i) scanf("%d",&c[i]); for(int i=0;i<=n;++i){ if(c[i]>c[i+1]) Add(s,i+1,c[i]-c[i+1],0); else Add(i+1,t,c[i+1]-c[i],0); if(i>0) Add(i+1,i,inf,0); } for(int i=1,l,r,d;i<=m;++i){ scanf("%d%d%d",&l,&r,&d); Add(r+1,l,inf,-d); printf("%d\n",-mcmf::dinic(s,t)); return 0; }
-
首先排序使得 \(S_{i,1}\le S_{i,2}\le \dots \le S_{i,k_i}\)。
记 \(x_{i,j}\) 表示第 \(i\) 家珠宝店的前 \(j-1\) 种珠宝被选择的次数之和,那么有:
- \(0=x_{i,1}\le x_{i,2}\le x_{i,3}\le \dots \le x_{i,k_i+1}=A\)。
- \(x_{i,j+1}-x_{i,j}\le c_{i,j}\)
- 对于限制 \((u_i,v_i,w_i)\),对于 \(\forall j\in[1,k_{v_i}]\),记 \(t\) 为最小的 \(t\) 使得 \(S_{u_i,t}+w_i\ge S_{v_i,j}\),那么如果在珠宝盒 \(v_i\) 选择 \((v_i,j\sim k_{v_i})\),\(u_i\) 珠宝盒只能选择 \(t\sim k_{u_i}\),因此有 \(x_{u_i,t}\le x_{v_i,j}\)。
可以发现以上就是原问题的充要条件,最终要求最小化 \(\sum_{i,j}p_{i,j}(x_{i,j+1}-x_{i,j})\),
与上一个问题基本相同,可以写作以下形式:
\[\min \sum_{i,j}p_{i,j}(x_{i,j+1}-x_{i,j})+\sum_{i,j}\left(+\infty(\max(x_{i,j}-x_{i,j+1},0+\max(x_{i,j+1}-x_{i,j}-c_{i,j},0)\right)+\sum_i\infty x_{i,1}\\ +\sum_i\infty\left(\max(x_{i,k_i+1}-A,0)+\max(A-x_{i,k_i+1},0)\right)+\sum_{u,v,w,j}\infty\max(x_{u,w}-x_{v,j},0) \]对偶后变成最小费用流,对 \((i,1)\) 的出度减入度要求为 \(+\infty\),其余点要求流量平衡。前 \(2\) 个 \(\sum\) 以及最后一个 \(\sum\) 都正常连边即可,第 \(4\) 个 \(\sum\) 可以看作 \(x_{i,k_i+1}-x_{i,1}-A\),于是相当于从 \((i,1)\) 向 \((i,k_i+1)\) 连流量为 \(+\infty\),费用为 \(A\) 的边;从 \((i,k_i+1)\) 向 \((i,1)\) 连流量为 \(+\infty\),费用为 \(-A\) 的边。因此可以将 \((i,1)\) 作为源点,\((i,k_i+1)\) 向汇点连费用为 \(-A\) 的边,就变成一个最小费用流问题了。
对于多组询问,考虑最终的费用中,\(A\) 只在 \((i,k_i+1)\) 连向汇点的边上作为费用,并且所有连向汇点的边的费用均为 \(-A\),因此最终答案一定形如 \(A\times flow-C\) 的形式,其中 \(flow\) 为流量,\(C\) 为此时其他边的费用。注意到最小费用流的过程形如不断跑最短路,跑出 \(A=0\) 时的 \(dis_t\),如果 \(dis_t\ge A\) 那么就终止,否则将本次增广的流加入答案。因此可以预处理出 \(A=0\) 时每次跑出的 \(dis_t\),以及此时的流量及费用,那么对于每组询问可以通过二分找到对应的 \(flow\) 为多少,即可回答询问。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int T=40,M=5e4+10,N=5e3+10,inf=1e9; ll Inf; namespace mcmf{ ll dis[N]; int tot,s,t,first[N],cnt=1,vis[N],cur[N]; struct node{ int u,v,f,nxt;ll c; }e[M*2]; inline void add(int u,int v,int f,ll c){ e[++cnt].u=u;e[cnt].v=v;e[cnt].f=f;e[cnt].c=c; e[cnt].nxt=first[u]; first[u]=cnt; } inline void Add(int u,int v,int f,ll c){ add(u,v,f,c); add(v,u,0,-c); } inline bool SPFA(){ queue<int>q; memset(dis+1,0x3f,sizeof(ll)*(tot)); if(!Inf) Inf=dis[1]; dis[s]=0;vis[s]=1; q.push(s); while(!q.empty()){ int u=q.front();q.pop();vis[u]=0; for(int i=first[u];i;i=e[i].nxt){ int v=e[i].v; if(e[i].f>0&&dis[v]>dis[u]+e[i].c){ dis[v]=dis[u]+e[i].c; if(!vis[v]){vis[v]=1;q.push(v);} } } } return dis[t]<Inf; } inline int dfs(int u,int f){ if(u==t||!f) return f; int used=0;vis[u]=1; for(int& i=cur[u];i;i=e[i].nxt){ int v=e[i].v; if(!e[i].f||vis[v]||dis[v]!=dis[u]+e[i].c) continue; int fl=dfs(v,min(f,e[i].f)); e[i].f-=fl;e[i^1].f+=fl; used+=fl;f-=fl; if(!f) break; } if(f) dis[u]=Inf; return vis[u]=0,used; } } using namespace mcmf; struct jew{ int s,p;ll c; }a[T][T]; struct pt{ ll k,c,f; }; inline bool operator <(const pt &x,const pt &y){ return x.k<y.k; } vector<pt> vec; int n,k[T],pos[T][T]; int main(){ scanf("%d",&n); s=++tot,t=++tot; for(int i=1;i<=n;++i){ scanf("%d",&k[i]); for(int j=1;j<=k[i];++j) scanf("%d%d%lld",&a[i][j].s,&a[i][j].p,&a[i][j].c); sort(a[i]+1,a[i]+k[i]+1,[&](const jew &x,const jew &y){ return x.s<y.s; }); pos[i][1]=s; for(int j=2;j<=k[i]+1;++j){ pos[i][j]=++tot; Add(pos[i][j-1],pos[i][j],a[i][j-1].p,0); Add(pos[i][j-1],pos[i][j],inf,a[i][j-1].c); Add(pos[i][j],pos[i][j-1],inf,0); } Add(pos[i][k[i]+1],t,inf,0); } int m;scanf("%d",&m); for(int i=1,u,v,w;i<=m;++i){ scanf("%d%d%d",&u,&v,&w); int t=1; for(int j=1;j<=k[v];++j){ while(t<=k[u]&&a[u][t].s+w<a[v][j].s) ++t; Add(pos[v][j],pos[u][t],inf,0); } } ll mxf=0;ll mxc=0; while(SPFA()){ memset(vis+1,0,sizeof(int)*(tot+1)); memcpy(cur,first,sizeof(int)*(tot+1)); vec.push_back((pt){dis[t],mxc,mxf}); int flow=dfs(s,inf); if(flow>inf/2) break; mxf+=flow;mxc+=dis[t]*flow; } int q;scanf("%d",&q); while(q--){ ll a;scanf("%lld",&a); auto t=lower_bound(vec.begin(),vec.end(),(pt){a,0,0}); if(t==vec.end()) puts("-1"); else printf("%lld\n",(*t).f*a-(*t).c); } return 0; }