[关闭]
@winee 2019-06-29T18:59:42.000000Z 字数 8406 阅读 1029

自选题:[NOI2008]志愿者招募

LHX

题目描述

申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管。布布刚上任就遇到了一个难题:为即将启动的奥运新项目招募一批短期志愿者。经过估算,这个项目需要天才能完成,其中第天至少需要个人。 布布通过了解得知,一共有类志愿者可以招募。其中第类可以从第 天工作到第天,招募费用是每人 元。新官上任三把火,为了出色地完成自己的工作,布布希望用尽量少的费用招募足够的志愿者,但这并不是他的特长。于是布布找到了你,希望你帮他设计一种最优的招募方案。

输入格式:
第一行包含两个整数, ,表示完成项目的天数和可以招募的志愿者的种类。 接下来的一行中包含N 个非负整数,表示每天至少需要的志愿者人数。 接下来的 行中每行包含三个整数, , ,含义如上文所述。为了方便起见,我们可以认为每类志愿者的数量都是无限多的。

输出格式:
仅包含一个整数,表示你所设计的最优方案的总费用。

输入样例#1:
3 3
2 3 4
1 2 2
2 3 5
3 3 2
输出样例#1:
14

说明
,题目中其他所涉及的数据均不超过

分析与建模

设第 天需要 个志愿者,记 为第 天第 个志愿者是否能工作。
题目要求总费用最低,即求:
它们需要满足要求:

这明显是一个的线性规划问题,但它不是标准形式,需要一些改造。

方法一 转化为对偶问题

观察它的对偶问题:
, 为第 个志愿者第 天是否能工作,新的约束条件为:

添加松弛变量后为:


这是线性规划的标准形式,对偶问题可以使用原始单纯型法求解。
根据对偶定理,若原问题有最优解,那么对偶问题也有最优解,且目标函数值相等。题目要的只有目标函数值,所以可以直接求解对偶问题,但这种方法得到的解不是原问题的解,所以这种方法实际上不太合适,不过它只要最基本的单纯形法,故放第一位。
代码如下:

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cmath>
  4. using namespace std;
  5. const int M=10005,N=1005,INF=1e9;
  6. const double eps=1e-7;
  7. int n,m;
  8. double a[M][N],b[M],c[N],v,cc[N];
  9. int B[N],P[M];
  10. void Out() {
  11. for (int i = 1; i <= m; i++) printf("%4d",P[i]);
  12. puts("");
  13. for (int i = 1; i <= m; i++) {
  14. for (int j = 1; j <= n; j++)
  15. printf("%4d",(int)a[i][j]);
  16. printf(" B:%4d b:%4d\n",B[i],(int)b[i]);
  17. }
  18. for (int i = 1; i <= n; i++)
  19. printf("%4d", (int)c[i]);
  20. puts("\nEnd");
  21. }
  22. void pivot(int l,int e) { //旋转运算,l换出变量 e换入变量
  23. swap(B[l],P[e]);
  24. b[l]/=a[l][e];
  25. for (int j=1;j<=n;j++) if(j!=e) a[l][j]/=a[l][e];
  26. // 行变换,处理l对应行,将a[l][e]变为1
  27. a[l][e]=1/a[l][e];
  28. for (int i=1;i<=m;i++) if(i!=l&&fabs(a[i][e])>0) { //其他行,e对应列,除a[l][e]外变为0
  29. b[i] -= a[i][e]*b[l];
  30. for(int j=1;j<=n;j++) if(j!=e) a[i][j]-=a[i][e]*a[l][j];
  31. a[i][e]=-a[i][e]*a[l][e];
  32. }
  33. //需要注意的是,e列将对应新的非基变量,即原基变量l对应的列
  34. v += c[e]*b[l]; //x的检验数即x值增加1对答案的影响,新的基变量值为b[l]
  35. for (int j=1;j<=n;j++) if(j!=e) c[j] -= c[e]*a[l][j]; //更新检验数
  36. c[e] = -c[e]*a[l][e];
  37. }
  38. double simplex() {
  39. while(true) {
  40. int e=1,l=0;//l换出变量 e换入变量
  41. for(int t=2; t<=n; t++) if(c[t]>c[e]) e = t; //找到检验数最大的变量
  42. if (c[e] < eps) return v;//所以检验数<=0,最优解的情况
  43. double mn=INF;
  44. for(int i=1;i<=m;i++) //找换出变量,a[i][e]>0且b[i]/a[i][e]最小
  45. if(a[i][e]>eps&&mn>b[i]/a[i][e]) mn=b[i]/a[i][e],l=i;
  46. if(mn==INF) return INF;//无可行解
  47. pivot(l,e);
  48. // Out();
  49. }
  50. }
  51. int main() {
  52. cin >> n >> m;
  53. for (int i=1; i<=n; i++) {
  54. cin >> cc[i];
  55. c[i] = cc[i];
  56. }
  57. for (int i=1; i<=m; i++) {
  58. int s,t; cin >> s >> t;
  59. for (int j=s; j<=t; j++) a[i][j]=1;
  60. cin >> b[i];
  61. P[i] = i;
  62. }
  63. for (int i = 1; i <= n; i++) B[i] = m+i;
  64. //Out();
  65. printf("%d",(int)simplex());
  66. }

方法二 人工变量法

在加入剩余变量后,分别给每个约束方程加入人工变量,得到初始基可行解。

1. 大M法

人工变量不能影响目标函数取值,必须从基变量中换出来。为此,可以在目标函数中把人工变量的系数设成M(一个充分大的数/惩罚系数),使得在求Min的过程中,人工变量变为非基变量可以大大减小目标函数值

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cmath>
  4. using namespace std;
  5. typedef long long ll;
  6. const int M=12005,N=1005,INF=1e5;
  7. const double eps=1e-6;
  8. int n,m;
  9. double a[N][M],b[N],c[M],v,cc[M];
  10. int B[N];
  11. void pivot(int l,int e) {
  12. b[l]/=a[l][e];
  13. for(int j=1;j<=n;j++) if(j!=e) a[l][j]/=a[l][e];
  14. a[l][e]=1;
  15. for(int i=1;i<=m;i++) if(i!=l&&fabs(a[i][e])>0) {
  16. b[i] -= a[i][e]*b[l];
  17. for(int j=1;j<=n;j++) if(j!=e) a[i][j]-=a[i][e]*a[l][j];
  18. a[i][e]=0;
  19. }
  20. for (int j=1;j<=n;j++) if(j!=e) c[j] -= c[e]*a[l][j];
  21. c[e] = 0;
  22. B[l] = e;
  23. }
  24. void Out() {
  25. puts("");
  26. for (int i = 1; i <= m; i++) {
  27. for (int j = 1; j <= n; j++)
  28. printf("%4d",(int)a[i][j]);
  29. printf(" B:%4d b:%4d\n",B[i],(int)b[i]);
  30. }
  31. for (int i = 1; i <= n; i++)
  32. printf("%4d", (int)c[i]);
  33. puts("\nEnd");
  34. }
  35. double simplex() {
  36. for (int i = 1; i <= n; i++) {
  37. cc[i] = c[i];
  38. for (int j = 1; j <= m; j++) {
  39. c[i] = c[i]-INF*a[j][i];
  40. }
  41. }
  42. while(true) {
  43. int e=0,l=0;
  44. for (int t=1; t<=n; t++)
  45. if (c[t]<-eps) {
  46. e = t;
  47. break;
  48. }
  49. if (e == 0) {
  50. v = 0;
  51. for (int i = 1; i <= m; i++) v += b[i]*cc[B[i]];
  52. return v;
  53. }
  54. double mn=INF;
  55. for(int i=1;i<=m;i++)
  56. if(a[i][e]>eps && mn>b[i]/a[i][e]) mn=b[i]/a[i][e],l=i;
  57. if(mn==INF) return INF;
  58. pivot(l,e);
  59. //Out();
  60. }
  61. }
  62. int main() {
  63. cin >> n >> m;
  64. for (int i=1; i<=n; i++) cin >> b[i];
  65. for (int i=1; i<=m; i++) {
  66. int s,t; cin >> s >> t;
  67. for (int j=s; j<=t; j++) a[j][i]=1;
  68. cin >> c[i];
  69. }
  70. for (int i = 1; i <= n; i++) {
  71. a[i][m+i] = -1;
  72. a[i][m+n+i] = 1;
  73. B[i] = m+n+i;
  74. }
  75. for (int i = m+n+1; i <= m+n+n; i++) c[i] = INF;
  76. m += n+n;
  77. swap(n,m);
  78. //Out();
  79. printf("%d",(int)simplex());
  80. }

2. 两阶段法

两阶段法则更为直接,第一步,目标函数为,即尽量使人工变量取值为0,如果,说明人工变量换不出去,即无解。若,说明有可行解,进行第二阶段,把矩阵中人工变量部分删掉,目标函数换回原问题的,即

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cmath>
  4. using namespace std;
  5. typedef long long ll;
  6. const int M=12005,N=1005,INF=1e5;
  7. const double eps=1e-6;
  8. int n,m;
  9. int a[N][M],b[N],c[M],v,cc[M],A[M];
  10. int B[N];
  11. void pivot(int l,int e) {
  12. b[l]/=a[l][e];
  13. for(int j=1;j<=n;j++) if(j!=e) a[l][j]/=a[l][e];
  14. a[l][e]=1;
  15. for(int i=1;i<=m;i++) if(i!=l&&fabs(a[i][e])>0) {
  16. b[i] -= a[i][e]*b[l];
  17. for(int j=1;j<=n;j++) if(j!=e) a[i][j]-=a[i][e]*a[l][j];
  18. a[i][e]=0;
  19. }
  20. for (int j=1;j<=n;j++) if(j!=e) c[j] -= c[e]*a[l][j];
  21. c[e] = 0;
  22. B[l] = e;
  23. }
  24. void Out() {
  25. puts("");
  26. for (int i = 1; i <= m; i++) {
  27. for (int j = 1; j <= n; j++)
  28. printf("%4d",(int)a[i][j]);
  29. printf(" B:%4d b:%4d\n",B[i],(int)b[i]);
  30. }
  31. for (int i = 1; i <= n; i++)
  32. printf("%4d", (int)c[i]);
  33. puts("\nEnd");
  34. }
  35. int simplex() {
  36. while(true) {
  37. int e=0,l=0;
  38. for (int t=1; t<=n; t++)
  39. if (c[t]<-eps) {
  40. e = t;
  41. break;
  42. }
  43. if (e == 0) {
  44. v = 0;
  45. for (int i = 1; i <= m; i++) {
  46. v += b[i]*cc[B[i]];
  47. }
  48. return v;
  49. }
  50. int mn=INF;
  51. for(int i=1;i<=m;i++)
  52. if(a[i][e]>eps && mn>b[i]/a[i][e]) mn=b[i]/a[i][e],l=i;
  53. if(mn==INF) return INF;
  54. pivot(l,e);
  55. //Out();
  56. }
  57. }
  58. int main() {
  59. cin >> n >> m;
  60. for (int i=1; i<=n; i++) cin >> b[i];
  61. for (int i=1; i<=m; i++) {
  62. int s,t; cin >> s >> t;
  63. for (int j=s; j<=t; j++) a[j][i]=1;
  64. cin >> A[i];
  65. }
  66. for (int i = 1; i <= n; i++) {
  67. a[i][m+i] = -1;
  68. a[i][m+n+i] = 1;
  69. B[i] = m+n+i;
  70. }
  71. for (int i = m+n+1; i <= m+n+n; i++) cc[i] = c[i] = 1;
  72. m += n+n;
  73. swap(n,m);
  74. //Out();
  75. for (int i = 1; i <= n; i++)
  76. for (int j = 1; j <= m; j++) {
  77. c[i] = c[i]- 1*a[j][i];
  78. }
  79. if (simplex() < eps) {
  80. n -= m;
  81. for (int i = 1; i <= n; i++) c[i] = cc[i] = A[i];
  82. for (int i = 1; i <= n; i++)
  83. for (int j = 1; j <= m; j++) {
  84. c[i] = c[i]- cc[B[j]]*a[j][i];
  85. }
  86. printf("%d",(int)(simplex()));
  87. } else {
  88. puts("Unsolvable!");
  89. }
  90. return 0;
  91. }

方法三 对偶单纯形法

将所有式子同乘后得到类似标准形式,但是b列出现负数,不能用原始单纯形法。
整理一下式子:


这里只介绍一下步骤。
(1)对线性规划问题是所有检验数<=0,即对偶问题为基可行解。(本题初始既满足)。
(2)检验:若b列都非负,检验数都非正,已达到最优解,停止计算。
(3)按对应的基变量为换出变量
(4)检查行各系数,若所有,说明无解。否则,计算,以对应的 为换入变量(规则保证对偶问题的解仍为可行解)。
(5)以为主元素,进行迭代,得到新表。
重复(2)~(5)。

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cmath>
  4. using namespace std;
  5. const int M=12005,N=1205,INF=1e7;
  6. const double eps=1e-6;
  7. int n,m;
  8. double a[N][M],b[N],c[M],v,cc[M],A[N];
  9. int B[N];
  10. void pivot(int l,int e) {
  11. b[l]/=a[l][e];
  12. for(int j=1; j<=n; j++)
  13. if(j!=e)
  14. a[l][j]/=a[l][e];
  15. a[l][e]=1;
  16. for(int i=1; i<=m; i++)
  17. if(i!=l&&fabs(a[i][e])>0) {
  18. b[i] -= a[i][e]*b[l];
  19. for(int j=1; j<=n; j++)
  20. if(j!=e)
  21. a[i][j]-=a[i][e]*a[l][j];
  22. a[i][e]=0;
  23. }
  24. for (int j=1; j<=n; j++)
  25. if(j!=e)
  26. c[j] -= c[e]*a[l][j];
  27. c[e] = 0;
  28. B[l] = e;
  29. }
  30. void Out() {
  31. puts("");
  32. for (int i = 1; i <= m; i++) {
  33. for (int j = 1; j <= n; j++)
  34. printf("%4d",(int)a[i][j]);
  35. printf(" B:%4d b:%4d\n",B[i],(int)b[i]);
  36. }
  37. for (int i = 1; i <= n; i++)
  38. printf("%4d", (int)c[i]);
  39. puts("\nEnd");
  40. }
  41. double dsimplex() {
  42. while (true) {
  43. int e=1,l=0;
  44. for (int t=2; t<=m; t++)
  45. if (b[t] < b[e]) e = t;
  46. if (b[e] > -eps) {
  47. v=0;
  48. for (int i = 1; i <= m; i++) v += b[i]*cc[B[i]];
  49. //for (int i = 1; i <= m; i++) v += (-c[n-m+i])*A[i];
  50. return v;
  51. }
  52. double mn=INF;
  53. for (int i = 1; i <= n; i++)
  54. if (a[e][i] < -eps && mn > c[i]/a[e][i])
  55. mn=c[i]/a[e][i],l=i;
  56. if (mn==INF) return INF;
  57. pivot(e,l);
  58. // Out();
  59. }
  60. }
  61. int main() {
  62. cin >> n >> m;
  63. for (int i=1; i<=m; i++) {
  64. cin >> A[i];
  65. b[i] = -A[i];
  66. }
  67. for (int i=1; i<=m; i++) {
  68. int s,t;
  69. cin >> s >> t;
  70. for (int j=s; j<=t; j++)
  71. a[j][i] = -1;
  72. cin >> cc[i];
  73. c[i] = -cc[i];
  74. }
  75. for (int i = 1; i <= n; i++) {
  76. a[i][n+i] = 1;
  77. B[i] = n+i;
  78. }
  79. m += n;
  80. swap(n,m);
  81. //Out();
  82. printf("%d",(int)(dsimplex()));
  83. return 0;
  84. }

一些补充说明

1.这四个程序在运算速度上没有本质上的差别,实际测试中人工变量法稍慢。
2.原问题的线性规划模型是一步到位的,但不是标准形式,具体的解决方法有很多,可见线性规划与单纯形法内容丰富,体系比较完备。
3.在已有的题解中,绝大多数作者是使用网络流来做。本题费用流的建图方法颇有挑战性,而线性规划模型简单很多,程序实现上单纯形法稍微麻烦一点,而这两类方法时间复杂度都比较高而且玄学,难分高下。总的来说,线性规划的方法更方便。
供参考的资料:
https://10420.blog.luogu.org/solution-p3980
https://blog.csdn.net/little_cats/article/details/81189794

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注