约束条件下的优化问题
约束条件下的优化问题
给定一系列训练,每个训练有以下几个属性:
名字 能量消耗 可以被选的阶段个数 阶段1 阶段2 ……
有x个阶段,每个阶段又有如下以下两个限制:
1.每个阶段中最多不能包含超过n个训练,也就是说训练个数小于等于n;
2.每个阶段里的能量消耗总和不得大于m。
在这种约束条件下,求得所有阶段能耗消耗总和最大的组合。每个训练最多可以被选择一次,可以不被选择。
这个问题咋一看有点类似于背包问题,每个训练最多可以被选择一个,有点像是01背包问题,并且也有限制条件,比如每个阶段不得大于n个训练,能耗之和不得大于m,但最终也想求得所有阶段能耗最大的组合。
背包问题有很多种,诸如01背包、完全背包、多重背包、混合背包、分组背包、多维背包等等。背包问题可以用动态规划(DP)来解决,有关背包问题的求解可以谷歌百度之,另外有篇背包问题的教程可以学习一下《背包问题九讲》。
上面这个问题有点类似于01背包、分组背包、多维背包方面的问题。但是由于博主还未对背包问题以及动态规划进行完整的研究和学习。所以,这里我们不采用按照背包问题的解法来试图求解该问题。
这里,我们是运用穷举的方法来解决该问题。
首先我们来看一下之前我们曾研究过的一个问题。有n组数,每一组中又有Mi个数,每组数中的个数有可能互不相同。比如有以下几组数:
1 2 3
4 5
6 7 8 9
我们想从每组数中选择一个数,组成一个序列,问有多少种组合,每种组合分别是什么。
如果,我们预先知道了有几组数,那么可以预先用循环来解决,有几组数,就用几个循环来解决。
但是,如果这些组的个数是未知的该怎么办呢。显然写好的n个循环是不适用的,因为写好的程序是死的,而给定的几组数是获得,组数一变,原来的程序就不适用了。
我们之前给出了一个递归解法:《从每组中依次选择一个元素》。我们的递归结束条件是当走到了最后一组时结束,递归调用条件是选择当前组中的一个后,再从后面的组中选择数。而循环是针对当前组内所有元素进行遍历。每个组中都要选择一个数,得到的每个结果序列中,都要有且只有每个组中的一个数。
通过对选数的分析,我们可以看出选数问题和这个问题有点类似,只不过是这个问题有几个约束条件:
1.每个阶段的训练个数小于等于n
2.每个阶段的训练总能耗小于等于m
3.每个训练有指定可以被选的阶段,并不是每个阶段都能选择任意个训练
另外,每个训练可以不被选择,或最多被选择一次。还有就是我们想到是的所有阶段总的能耗最大的结果。
我们将每个训练看做每组数,而训练中的元素就是该训练可以被选择的阶段名或索引,另外还包括不被选择的情形。得到的结果相当于选数得到的序列。
只不过,在选择的时候,我们需要依次检测上述三个约束条件,是的结果都能满足这三个约束条件。另外,我们还要考虑每个训练被选择和不被选择的情况。最后,到得到一个结果后,我们还要检测该结果是否是最优结果,需要将最后结果保存。
下面我们依照程序进行讲解。
我们求解该问题的函数式solt函数,该函数存在两个递归调用。首先,针对第一个约束条件,我们会检测当前阶段的联系个数是否小于n。针对第二个约束条件,检测如果将该训练加入到该阶段,那么该阶段的能耗总和是否小于等于m。另外,在检测前面两个条件之前,我们会根据训练可以被选择的阶段所以,获取其可以被选择的阶段索引,这个对应于第三个约束条件。
另外,如果可以被选择,那么我们会将该训练加入到该阶段,进行递归调用,递归调用完之后,我们还要将其退出,继续检测下一个阶段。
由于,每个训练可能存在不被选择的情形,所以,我们不管是否能不能被选进去检测阶段,都要不选择该训练的前提下进行递归调用,直接检测下一个训练。
所以,循环内部有两个递归调用。而选数问题中,我们不用检测约束条件,直接选中。也就是说,选中的情形占100%。而这个问题中,符合条件被选中这种情形占50%。另外,符合条件也可以不选中,所以不选中还有50%。另外是不符合条件就不选中还有50%。所以递归调用的个数是150%,被选中是50%。而选数递归调用是100%,被选中是100%。
循环是针对当前训练下,遍历其可以被选中的阶段。递归调用条件时处理完(选中和不选中)后,调用下一个训练的递归调用。递归终止条件是,当检测完所有的训练后,就终止。
终止时,需要检测得到的结果是否是最优结果,如果是最优的,那么情况所有原来的,并保存该最优结果;如果和当前最优的一样好,那么检测该新的结果是否与已保存的最优结果重复,如果重复,则直接返回,如果不重复,则将其也保存到结果集中。之所以存在重复的结果是因为,当检测到后面是,后面的训练不管入选到哪个阶段都会破坏约束条件,所以前面已选的结果就会存在重复性。这就需要我们进行检测,以防止最优结果中有重复情形。
下面我们给出该问题的一个样例,有数据文件:
n和m由用户指定。我们给句给定的训练数据信息和n、m约束得到最优的结果。下面给出程序实现。具体相关细节可以查看程序代码和注释说明。
// 约束最优 #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX (20 + 1) #define NUM (10 + 1) // exercise结构体 typedef struct exercise { char name[MAX]; // 名称 int enexp; // 能耗 int n_phase; // 适用于phase的个数 int phases[NUM]; // phase的索引 int flag; // 是否已经被选择,0表示未被选择,1表示已选择,没用到 } Exercise; // phase结构体 typedef struct phase { int indexes[NUM]; // 保存exercise的索引 int n_exercise; // exercise的数目 int totalenexp; // 该phase的体能消耗和 } Phase; // 读取文件 Exercise* readfile(char* filename, int* n_exercise) { int i = 0, j = 0, eng = 0, n = 0, tmp = 0; Exercise* rt = NULL; FILE* fp = NULL; char name[MAX]; fp = fopen(filename, "r"); if (fp == NULL) { fprintf(stderr, "Open file error!\n"); exit(1); } if (fscanf(fp, "%d", n_exercise) == EOF) { fprintf(stderr, "Error in reading file!\n"); exit(1); } rt = (Exercise*)malloc(*n_exercise * sizeof (Exercise)); if (rt == NULL) { fprintf(stderr, "Error in allocating the memory!\n"); exit(1); } for (i = 0; i < *n_exercise; ++i) { fscanf(fp, "%s %d %d", name, &eng, &n); strcpy(rt[i].name, name); rt[i].enexp = eng; rt[i].n_phase = n; rt[i].flag = 0; // 初始化flag for (j = 0; j < n; ++j) { fscanf(fp, "%d", &tmp); rt[i].phases[j] = tmp; } } // 读取完毕,关闭文件 fclose(fp); // 返回rt return rt; } // 初始化phases void initphases(Phase* ph, int n) { int i = 0; for (i = 1; i <= n; ++i) { ph[i].n_exercise = 0; ph[i].totalenexp = 0; } } // 打印一个结果 void printphases(Phase* ph, int n, Exercise* rt) { int i = 0, j = 0; int idx = 0; int total = 0; for (i = 1; i <= n; ++i) { printf("PHASE %d\n", i); total = 0; for (j = 0; j < ph[i].n_exercise; ++j) { idx = ph[i].indexes[j]; printf("\t%s\t%d\n", rt[idx].name, rt[idx].enexp); total += rt[idx].enexp; } printf("Total:%d\n", total); } printf("\n"); } // 计算总的能量消耗 int totoalenergy(Phase* ph, int n, Exercise* rt) { int i = 0, j = 0; int idx = 0; int total = 0; for (i = 1; i <= n; ++i) { for (j = 0; j < ph[i].n_exercise; ++j) { idx = ph[i].indexes[j]; total += rt[idx].enexp; } } return total; } // 打印所有exercises void printexercise(Exercise* rt, int n) { int i = 0, j = 0; for (i = 0; i < n; ++i) { printf("%s\t%d\t%d\t", rt[i].name, rt[i].enexp, rt[i].n_phase); for (j = 0; j < rt[i].n_phase; ++j) { printf("%d\t", rt[i].phases[j]); } printf("%d\n", rt[i].flag); } printf("\n"); } // 判断结果是否一致 int issame(Phase ph1[5], Phase ph2[5], int n) { int i = 0, j = 0; int idx1 = 0, idx2 = 0; for (i = 1; i <= n; ++i) { if (ph1[i].n_exercise != ph2[i].n_exercise) { return 0; } for (j = 0; j < ph1[i].n_exercise; ++j) { idx1 = ph1[i].indexes[j]; idx2 = ph2[i].indexes[j]; if (idx1 != idx2) { return 0; } } } return 1; } // 求解所有的可能结果 void solt(Exercise* rt, int num1, int pos, Phase* ph, int num2, int n, int m, int* maxtotal, Phase ph2[100][5], int* count) { int i = 0, j = 0; int index = 0; int total = 0; if (pos >= num1) // 递归终止条件 { total = totoalenergy(ph, num2, rt); if (total > *maxtotal) // 如果得到的新结果比原来最好结果都好,则删除原来的所有结果,并将新结果保存 { *maxtotal = total; for (j = 1; j <= num2; ++j) { ph2[0][j] = ph[j]; } *count = 1; } else if (total == *maxtotal) // 如果得到的新结果与原来的新结果一样 { for (j = 0; j < *count; ++j) // 首先检测是否与原来的结果存在重复 { if (issame(ph2[j], ph, num2)) { return; } } for (j = 1; j <= num2; ++j) // 若不重复,则将新结果保存 { ph2[*count][j] = ph[j]; } ++*count; } return; } for (i = 0; pos < num1 && i < rt[pos].n_phase; ++i) // 顺序遍历每个exercise可以参加的phase { index = rt[pos].phases[i]; if (ph[index].n_exercise < n && ph[index].totalenexp + rt[pos].enexp <= m) // 如果符合条件,则加入到phase中 { ph[index].indexes[ph[index].n_exercise++] = pos; ph[index].totalenexp += rt[pos].enexp; solt(rt, num1, pos + 1, ph, num2, n, m, maxtotal, ph2, count); --ph[index].n_exercise; ph[index].totalenexp -= rt[pos].enexp; } solt(rt, num1, pos + 1, ph, num2, n, m, maxtotal, ph2, count); // 不管符不符合条件,都不加入到phase中 } } // 打印结果 void printmax(Phase maxph[100][5], int pos, int n, Exercise* rt) { int i = 0; for (i = 0; i < pos; ++i) { printphases(maxph[i], n, rt); } } int main(int argc, char* argv[]) { Exercise* rt = NULL; Phase ph[4 + 1], ph2[100][4 + 1]; Phase maxphs[100][4 + 1]; int count = 0; int maxtotoal = -1; int num1 = 0; int n = 0, m = 0; int i = 0; if (argc != 4) { fprintf(stderr, "Error in passing the arguments!\n"); exit(1); } n = atoi(argv[1]); m = atoi(argv[2]); rt = readfile(argv[3], &num1); initphases(ph, 4); solt(rt, num1, 0, ph, 4, n, m, &maxtotoal, ph2, &count); printf("%d %d\n", count, maxtotoal); for (i = 0; i < count; ++i) { printf("%d:\n", i + 1); printphases(ph2[i], 4, rt); printf("\n"); } printf("%d %d\n", count, maxtotoal); //printphases(ph, 4, rt); return 0; }
对选数的重写
下面我们给出选数问题的重写,与《从每组中依次选择一个元素》的不同在于,我们修改了索引,递归结束条件不是到达最后一个终止,而是将所有的都处理完后终止。
具体代码如下:
// 选数重写 #include <iostream> #include <vector> using namespace std; // 参数n和total可以省略 // 因为n=src.size() // total=dst.size() void SelectNumber(const vector<vector<int> >& src, int n, int pos, vector<vector<int> >& dst, int& total, vector<int>& stk) { if (pos >= n) { dst.push_back(stk); ++total; return; } for (auto i = 0; i != src[pos].size(); ++i) { //if (pos >= n) //{ // dst.push_back(stk); //} //else { stk.push_back(src[pos][i]); SelectNumber(src, n, pos + 1, dst, total, stk); stk.pop_back(); } } } int main() { vector<vector<int> > src; vector<int> tmp; tmp.push_back(1); tmp.push_back(2); tmp.push_back(3); src.push_back(tmp); tmp.clear(); tmp.push_back(4); tmp.push_back(5); src.push_back(tmp); tmp.clear(); tmp.push_back(6); tmp.push_back(7); tmp.push_back(8); tmp.push_back(9); src.push_back(tmp); tmp.clear(); vector<vector<int> > dst; int total = 0; SelectNumber(src, src.size(), 0, dst, total, tmp); for (auto i = 0; i != src.size(); ++i) { for (auto j = 0; j != src[i].size(); ++j) { cout << src[i][j] << ' '; } cout << endl; } cout << endl; for (auto i = 0; i != dst.size(); ++i) { for (auto j = 0; j != dst[i].size(); ++j) { cout << dst[i][j] << ' '; } cout << endl; } cout << endl; cout << total << endl; cout << endl; return 0; }
递归结束条件改为检查完所有的组,其返回位置改为了for循环外面,因为原来的检测到最后一个后,还需要处理最后一个,所以需要在循环内部。而检测完后,如果放在for循环里面,那么会越界,导致错误,并且放在里面没有任何意义。所以,我们需要将其放在外面,直接结束递归调用。
(完)
文档信息
·版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
·博客地址:http://www.cnblogs.com/unixfy
·博客作者:unixfy
·作者邮箱:goonyangxiaofang(AT)163.com
·如果你觉得本博文的内容对你有价值,欢迎对博主 小额赞助支持