程序最美(寻路)

你还在坚持练习你的技术吗?运动员天天训练,音乐家也会演练更难的曲章。你呢?

约束条件下的优化问题

约束条件下的优化问题

         给定一系列训练,每个训练有以下几个属性:

名字 能量消耗 可以被选的阶段个数 阶段阶段……

         有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循环里面,那么会越界,导致错误,并且放在里面没有任何意义。所以,我们需要将其放在外面,直接结束递归调用。

posted on 2013-12-26 00:38  unixfy  阅读(3320)  评论(0编辑  收藏  举报

导航