C语言程序设计 - 积分兑换

前言

大家好,又是我。

想必大家都有要兑换积分的时候,比如商场的购物积分、运营商那里的话费积分,抑或是学校的积分卡课程学习的点数等等,那么,假设商品 / 礼品不限量,我们怎么才能尽可能地把积分花到一分不剩呢?

其实这个也是老师布置的结课作业之一,我在网上搜索相关关键字,但好像没有看到类似的文章,于是想着来抛砖引玉一下。

下面把老师的作业要求放上来:

  1. 用户在购物网站上有消费积分m,最近网站推出积分换购的活动。换购商品的种类数是不确定的,如:“双肩包”下架了,但又增加了“香皂”、“牙刷”等。
  2. 每种商品换购的次数不限,如:换购“新款毛巾”10条、12条、100条,都可以。
  3. 用户积分换购后几乎都会有剩余,找出换购后积分损失最小的前5种组合。

具体实现过程

思路

个人不是很聪明,隐约感觉这个和“零钱兑换”有点相似,但不是很确定。

嗯,有人说这个可以用动态规划……抱歉,这个我不会(目前还搞不懂);只会枚举法。

嗯,那就枚举法了。

当然,我也知道,枚举法耗时很高,甚至可能等不到尽头,但好在我们只要求若干可行方案。实践表明,一般很快就可以计算得出需要的结果。

说到枚举法,那一定要用到循环啦,而如若是多种商品,那就要用到多重循环了;而这里有一个问题就是,商品的数目不定,那如何才能实现不定数量的多重循环呢?我决定采用递归的方法来实现。

同时,为了避免超时,我将程序设置成找到5个“最优解”——即剩余积分为0的结果——就终止计算并输出结果。然而,在和同学的交流的过程中,我发现一些比较特殊的情况,在简单的枚举法下还是容易超时;

a. 当遇到无论如何也会有剩余积分的时候,且积分总量很大(即商品价值全为10的倍数,而积分不是)时,不能终止运算,造成不必要的时间复杂度;
b. 当遇到商品价值全部相同,而积分不能被其整除,也会导致一直枚举完全部情况
c. 当商品价值全为偶数,而积分是奇数时;

还有一个不知道算不算bug的问题,就是商品的价值取值丰度不够,而积分过大,可能会导致超时(估计这是枚举法不可避免的缺陷)。

商品数据读入与存储

下面来写程序吧,我们先捡简单的做,就从数据读入存储做起;

首先,老师的要求是用结构体来存储数据,我这里定义了一个叫spInfo的数据类型;

struct spInfo
{
    
    
    char name[256];
    int  value;
};

然后定义一个结构体数组spList。商品的初始数据可以通过初始化的形式写进程序,比如这样:

#define MAX_SP_NUM 256
spInfo spList[MAX_SP_NUM] = {
    
    
	{
    
    "Towels",380},
	{
    
    "Tooth Paste",260},
	{
    
    "Pencil",80}
};

不过,有了之前学生信息管理大作业的经验,为何不做成从文件读取呢?这样方便修改,方便从网站上复制粘贴信息下来,总之较为用户友好;当然,也可以选择键盘输入。接下来我要介绍文件读入的方法,如果不需要请跳过接下来的部分。

首先,我们的文件需要写成这个样式,文件名这里设为了splist.txt

双肩包 = 8288
移动电源 = 7909
不锈钢保温杯 = 8699
新款毛巾 = 678
滋润型护肤霜 = 721

trim()str2num()是自己写的函数,用来修剪掉字符串首末的空格与换行。str2num()可以用stdlib.h里面的atoi()代替。详情参见我写的学生信息管理……

void trim(char* strIn, char* strOut) // support in-place opreation
{
    
    
	char *a=strIn, *b;
	while (*a == ' '||*a == '\n' || *a == '\t') a++; // ignore spaces at the beginning
	b = strIn + strlen(strIn) - 1; // get pointer pointing at the end of the line
	while (*b == ' '||*b == '\n' || *a == '\t') b--; // ignore spaces at the end
	while (a<=b) *strOut++ = *a++; // transplace
	*strOut='\0';
}

load_spList()用来执行文件读取。

int load_spList(const char* filePath = "splist.txt")
{
    
    
    FILE *fp = fopen(filePath,"r");
    char buffer[512]; //缓冲区,用来读入文件的一行
    char tmp[512]; //临时字符数组
    int k=0;
    while(!feof(fp))
    {
    
    
        fgets(buffer, sizeof(buffer), fp);
        char *src = buffer;
        char *dst = tmp;
        while (*src == ' ') src++; //ignore spaces
        if (*src =='\n') continue; //ignore empty line;

        trim(buffer,buffer); 
        while (*src != '=') *dst++ = *src++;
        *dst = '\0';
        trim(tmp,tmp);
        strcpy(spList[k].name,tmp);

        src = str2num(src, &spList[k].value);
        k++;
    }
    return k;
}

枚举的递归实现

下面声明了一些全局变量、数组,用来存储数据和结果;

#define RESULT_NUM 5
int results[RESULT_NUM][MAX_SP_NUM + 1];
int tmp_result[MAX_SP_NUM];
int results_index = 0;
int best_results = 0;
int EXPECT_REMAIN = 0;

RESULT_NUM 是用来限定要求输出方案的数目,results数组用来存储结果(其每行最后的那个位置用来存放剩余积分),tmp_result用来存储当前讨论过程中的商品兑换数量,results_index用来表示现在存放结果的数组中到了第几行,best_results用来记录已经找到的最优方案,EXPECT_REMAIN用来设定最佳方案应当剩余的积分数(应对上文提出的会引起超时的问题)。

下面放送核心函数a(),用来执行递归操作。函数的操作可以这样理解,对第1件商品,先假设买n件,积分减减,然后调用递归讨论下一件,买m件……直到到了出口条件:积分用光,或者达到最优的情况,或者商品枚举完毕。

据此,我们的函数应该直到这样几个事:

  • 当前讨论的商品序号
  • 总商品数
  • 剩余积分
  • 结果记录表 ——已设为全局变量

当然,我这里想到这几种,你也可以根据你的想法设置自己需要的变量。

注:update_plan()函数用来完成向结果存储数组的誊写。

void update_plan(int tot_sp, int now_sp_index, int now_credit, int results_index)
{
    
    
    //写入结果,同时避免写入tmp_result里面来自上一次讨论的无意义的结果
    for (int p = 0; p<= now_sp_index - 1; p++ ) results[results_index][p] = tmp_result[p];
    //把其他的部分置零,因为结果存储数组里可能会有之前没那么优秀的结果的数据
    for (int p = now_sp_index; p< tot_sp; p++) results[results_index][p]=0;
    //最后一个位置存放剩余的积分数
    results[results_index][MAX_SP_NUM] = now_credit;
}
void a(int tot_sp,int now_sp_index, int now_credit)
{
    
    
    if (best_results == RESULT_NUM) return; //已经找够了,就退出
    // 如果剩余积分达“最佳状态”,或者讨论完了所有商品:
    if (now_credit <= EXPECT_REMAIN || now_sp_index == tot_sp ) 
    {
    
    
    	//如果这是个最佳结果,则“最佳结果”数加一
        if (now_credit == EXPECT_REMAIN) best_results++; 
        //如果结果存储数组还没有满
        if (results_index < RESULT_NUM) 
        {
    
    
            update_plan(tot_sp,now_sp_index,now_credit,results_index);
            results_index++;
        }
        else //否则得看有没有可以替换的数据
        {
    
    
            int max_remain_i = 0;
            for (int w=0; w < RESULT_NUM; w++) //遍历结果数组,看谁的剩余积分最多
            {
    
    
                if (results[w][MAX_SP_NUM] > results[max_remain_i][MAX_SP_NUM]) max_remain_i = w;
            } 
            if(now_credit<results[max_remain_i][MAX_SP_NUM]) //如果当前找到的方案比它优秀,那么就更新
                update_plan(tot_sp,now_sp_index,now_credit,max_remain_i);
        }
        return;
    }
	//从当前商品能兑换的最大量开始讨论
    for (int i = now_credit/spList[now_sp_index].value; i >=0 ; i--) 
    {
    
    
        tmp_result[now_sp_index]=i; //假设购买i件当前商品
        //往下一层递归
        a(tot_sp, now_sp_index+1, now_credit - spList[now_sp_index].value*i);
        //递归函数终止后,会回到上一级,然后进入下一个循环,即商品数-1,然后接着讨论
    }
}

main函数

经过一些尝试性的运算可以得知,如果积分很多,商品价值的种类有一定丰度,很容易就能花光所有积分 ;如果积分不多,那么就算商品很多,整个枚举过程很快就能结束(可以想一下,因为兑换不了几个,所以递归很快就能终止)。

int main()
{
    
    
	//首先读入的商品的信息
    int tot_sp = load_spList();
	//设置三个标签,对应前文所提到的特殊情况
    int flag_1 = 1; //假设所有商品都是10的倍数
    int flag_2 = 1; //假设所有商品价值均相等
    int flag_3 = 1; //假设所有商品价值全为偶数
    //输出读取到的商品信息
    printf("# Items: \n");
    for (int i=0;i<tot_sp;i++)
    {
    
    
        printf("%5d | %s\n",spList[i].value,spList[i].name);
        
        if (spList[i].value%10 != 0) flag_1 = 0;
        if (i>0) if (spList[i].value != spList[i-1].value) flag_2 = 0;
        if (spList[i].value % 2 == 1) flag_3 = 0;
    }
    printf("# Total %d items;\n",tot_sp);
    
    //获取用户积分数
    int m;
    printf("# Please input the credits you've got: ");
    scanf("%d",&m);
    
	//设定“最佳剩余积分”,即积分不可能少于这个
    if (flag_1 == 1) EXPECT_REMAIN = m % 10; 
    if (flag_3 == 1 && flag_1 != 1) EXPECT_REMAIN = m % 2; 
    if (flag_2 == 1) EXPECT_REMAIN = m % spList[0].value;
    // 比如,积分不是10的倍数,但恰好商品全是10的倍数,
    // 则剩余积分不可能为0,最好情况也只可能是m%10。
    //benchmark(m,tot_sp,flag_1,flag_2,flag_3); 
    //跑分函数用来检测有没有会被卡住的情况,该函数的definition见下个部分
    
	//开始运算
    a(tot_sp,0,m); 
    //运算结束,输出结果
    
    printf("\n");
    int cnt = 1; //记录输出方案的个数
    for(int i=0;i<results_index;i++)
    {
    
    
    	if(results[i][MAX_SP_NUM]==m) continue; //跳过积分没有被使用的情况
        
        printf("# Plan %d:\n",cnt++); //输出方案序号
        int remain = m;
        for (int j=0;j<tot_sp;j++) 
        {
    
    
            if (results[i][j] == 0) continue; //不输出没有兑换的商品
            printf("- %5d * %9d | %s\n",results[i][j],spList[j].value,spList[j].name);
            //remain -= results[i][j] * spList[j].value; 
            //设置一个remain,用来校验是否计算正确,可以去掉它,直接使用数组中存储的那个结果
        }
        printf ("# Remain credits: %d\n", results[i][MAX_SP_NUM]);
        //printf ("# Remain credits: %d\n",remain);
        printf("\n");
    }
    printf("# Finished!\n");
    if(cnt == 0) printf("# No solution can be found!\n");
    return 0;
}

跑分:检测算法性能

加入了跑分函数,可以用来检测程序是否会被从1到m之间的某个数卡住。貌似没啥用。

int benchmark(int m, int tot_sp, int flag_1, int flag_2, int flag_3)
{
    
    
    if (flag_1 == 1) EXPECT_REMAIN = m % 10; 
    if (flag_3 == 1 && flag_1 != 1) EXPECT_REMAIN = m % 2; 
    if (flag_2 == 1) EXPECT_REMAIN = m % spList[0].value;
    for (int i=1;i<m;i++)
    {
    
       
        a(tot_sp,0,i);
        printf ("credits: %8d, Bingo! \n",i); 
        
        results_index = 0;
        best_results = 0;
        memset(results,0,sizeof(results));
        memset(tmp_result,0x00,sizeof(tmp_result));
    }
}

最后

可能有人说,你这个算法不行,结果都是随便找到的解,那我想兑换自己想要的怎么办?

对于这种情况,我觉得可以先把自己想要的东西兑换后,把积分减去,然后再次运行这个程序就好了;再者,我只负责帮你把积分花干净,没有说能够帮助你做出最心仪的选择呀。

如果你喜欢它,可以拿它去试一下,看能不能在生活中用到(估计没什么人用——“剩点积分怎么了,为了那点儿东西还要写个程序,太麻烦了!”)。还可以做一些优化,比如结果先排序再输出,比如可以预先把想要的商品“兑换”掉等等。总的来说,这个程序能够应付大多问题,虽然还是会有卡住的情况,如果你不幸 / 有幸遇到了,可以在下面留言。

猜你喜欢

转载自blog.csdn.net/henry_23/article/details/104971250
今日推荐