编程之美之买书有关问题
这个问题来自《编程之美》这本书,应该在微软面试中出现过。是一个典型的动态规划问题。
问题描述
《哈利波特》系列一共有五卷,每一卷售价均8欧元。同时买不同的卷(各一本)有折扣,具体如下表所示。
购买方案 | 折扣 |
2卷 | 5% |
3卷 | 10% |
4卷 | 20% |
5卷 | 25% |
设计一个算法,计算购书组合,使得所购买的一批书花费最少。
求解及分析
假设输入为:1本卷1,2本卷2,2本卷3,2本卷4,1本卷5
购买组合及其折扣
5*0.25+3*0.1=1.55
4*0.2+4*0.2=1.6
3*0.1+3*0.1+2*0.05=0.7
2*0.05+2*0.05+2*0.05+2*0.05=0.4
可以看出最优组合为4+4时折扣最大。
程序描述
IDE:vs2010
语言:c++
工程:win32控制台程序
基本程序文件
数据结构定义文件:CommonDefine.h
#pragma once #include <vector> #include <string> #include <map> using namespace std; //定义书籍信息 struct StBookInfo { StBookInfo() : strVolIdx("") , nBookCount(0) {} string strVolIdx;//卷描述 int nBookCount;//书数 bool operator<(const StBookInfo& bookInfo) const { return nBookCount < bookInfo.nBookCount; } bool operator>(const StBookInfo& bookInfo) const { return nBookCount > bookInfo.nBookCount; } }; typedef vector<StBookInfo> VctBookInfo; struct StBuyPlan { double dbOff;//折扣值 vector<string> vBuyBooks;//购买书籍列表 }; typedef vector<StBuyPlan> VctBuyPlan;算法采用策略模式,定义一个统一接口IBuyMethod.h。
#pragma once #include "CommonDefine.h" class IBuyMethod { public: virtual ~IBuyMethod() {} /* @brief 购买书籍方法 @param[in] vBookList 待购买列表 @param[out] vBuyPlan 购买折扣及组合 */ virtual void BuyBook(const VctBookInfo& vBookList, VctBuyPlan& vBuyPlan) = 0; };
对应运行类CBookBuy。
BookBuy.h
#pragma once #include "IBuyMethod.h" class CBookBuy { public: CBookBuy(void); ~CBookBuy(void); void Run(); private: void InitBookList(); private: IBuyMethod* m_pMethod; vector<StBookInfo> m_vBookList; };BookBuy.cpp
#include "StdAfx.h" #include "BookBuy.h" #include "GreedyBuy.h" #include "DynPrgBuy.h" #include "DynPrgBuyM.h" #include <iostream> using namespace std; CBookBuy::CBookBuy(void) { //算法采用了策略模式,方便随时替换 m_pMethod = new CGreedyBuy(); //m_pMethod = new CDynPrgBuy(); //m_pMethod = new CDynPrgBuyM(); InitBookList(); } CBookBuy::~CBookBuy(void) { delete m_pMethod; m_pMethod = NULL; m_vBookList.clear(); } void CBookBuy::Run() { VctBuyPlan vBuyPlan; m_pMethod->BuyBook(m_vBookList, vBuyPlan); double dbTotalOff = 0; VctBuyPlan::iterator vPlanIter = vBuyPlan.begin(); for (; vPlanIter != vBuyPlan.end(); ++vPlanIter) { double dbCurOff = vPlanIter->dbOff; int nCurCount = (int)vPlanIter->vBuyBooks.size(); double dbSubOff = dbCurOff*nCurCount; dbTotalOff += dbSubOff; cout<<"当前折扣:"<<dbSubOff<<"="<<dbCurOff<<"*"<<nCurCount; cout<<"\t购买组合:"; vector<string>::iterator vBookIter = vPlanIter->vBuyBooks.begin(); for (; vBookIter != vPlanIter->vBuyBooks.end(); ++vBookIter) { cout<<(*vBookIter)<<" "; } cout<<endl; } cout<<"总的折扣:"<<dbTotalOff<<endl; } void CBookBuy::InitBookList() { StBookInfo bookInfo; bookInfo.strVolIdx = "卷1"; bookInfo.nBookCount = 1; m_vBookList.push_back(bookInfo); bookInfo.strVolIdx = "卷2"; bookInfo.nBookCount = 2; m_vBookList.push_back(bookInfo); bookInfo.strVolIdx = "卷3"; bookInfo.nBookCount = 2; m_vBookList.push_back(bookInfo); bookInfo.strVolIdx = "卷4"; bookInfo.nBookCount = 2; m_vBookList.push_back(bookInfo); bookInfo.strVolIdx = "卷5"; bookInfo.nBookCount = 1; m_vBookList.push_back(bookInfo); }
可以调用来运行
CBookBuy buyTool;
buyTool.Run();
贪心算法
直观上想,每步都取折扣最大的组合,这样可以实现贪心算法。
GreedyBuy.h
#pragma once #include "IBuyMethod.h" //贪心购买算法 class CGreedyBuy : public IBuyMethod { public: CGreedyBuy(void); virtual ~CGreedyBuy(void); virtual void BuyBook(const VctBookInfo& vBookList, VctBuyPlan& vBuyPlan); private: //移除已空的书籍 void RemoveEmptyBook(VctBookInfo& vBookList); };GreedyBuy.cpp
#include "StdAfx.h" #include "GreedyBuy.h" CGreedyBuy::CGreedyBuy(void) { } CGreedyBuy::~CGreedyBuy(void) { } void CGreedyBuy::BuyBook(const VctBookInfo& vBookList, VctBuyPlan& vBuyPlan) { VctBookInfo vInputBook = vBookList; //贪心算法,确保当前步获取为最优值 double dbTotalSum = 0; while (true) { RemoveEmptyBook(vInputBook); if (vInputBook.empty()) { break; } int nVolCount = 0; vector<string> vCurBooks; VctBookInfo::iterator vIter = vInputBook.begin(); for (; vIter != vInputBook.end(); ++vIter) { ++nVolCount; --vIter->nBookCount; vCurBooks.push_back(vIter->strVolIdx); } double dbOff = 0; switch (nVolCount) { case 2: dbOff = 0.05; break; case 3: dbOff = 0.10; break; case 4: dbOff = 0.20; break; case 5: dbOff = 0.25; break; } StBuyPlan planInfo; planInfo.dbOff = dbOff; planInfo.vBuyBooks = vCurBooks; vBuyPlan.push_back(planInfo); } } void CGreedyBuy::RemoveEmptyBook(VctBookInfo& vBookList) { VctBookInfo::iterator vIter = vBookList.begin(); for (; vIter != vBookList.end();) { if (vIter->nBookCount > 0) { ++vIter; } else { vIter = vBookList.erase(vIter); } } }
运行结果如下
当前折扣:1.25=0.25*5 购买组合:卷1 卷2 卷3 卷4 卷5
当前折扣:0.3=0.1*3 购买组合:卷2 卷3 卷4
总的折扣:1.55
最大折扣为1.6,贪婪算法得到的结果并不是最优的。
基本动态规划
每卷的价格都一样,故算法可做简化。
F为总折扣率。
输入Y为每卷的书数。
其中Y1,Y2,Y3,Y4,Y5根据大小做过排序,Y1>Y2>Y3>Y4>Y5。
F(Y1,Y2,Y3,Y4,Y5)
= 0 if (Y1=Y2=Y3=Y4=Y5=0)
= max{
5*0.25+F(Y1-1,Y2-1,Y3-1,Y4-1,Y5-1), if (Y5>=1)
4*0.20+F(Y1-1,Y2-1,Y3-1,Y4-1,Y5), if (Y4>=1)
3*0.10+F(Y1-1,Y2-1,Y3-1,Y4,Y5), if (Y3>=1)
2*0.05+F(Y1-1,Y2-1,Y3,Y4,Y5), if (Y2>=1)
1*0+F(Y1-1,Y2,Y3,Y4,Y5), if (Y1>=1)
}
动态规划,获取最大的折扣。
例:
F(1,2,2,2,1)
= max{
5*0.25+F(0,1,1,1,0),
4*0.20+F(0,1,1,1,1),
3*0.10+F(0,1,1,2,1),
2*0.05+F(0,1,2,2,1),
1*0+F(0,2,2,2,1),
}
//等价于,排序结果
= max{
5*0.25+F(1,1,1,0,0),
4*0.20+F(1,1,1,1,0),
3*0.10+F(2,1,1,1,0),
2*0.05+F(2,2,1,1,0),
1*0+F(2,2,2,1,0),
}
源码DynPrgBuy.h。
#pragma once #include "IBuyMethod.h" //动态规划算法求解 class CDynPrgBuy : public IBuyMethod { public: CDynPrgBuy(void); ~CDynPrgBuy(void); virtual void BuyBook(const VctBookInfo& vBookList, VctBuyPlan& vBuyPlan); private: double BuyByDyn(VctBookInfo vBookList, VctBuyPlan& vBuyPlan); //移除已空的书籍 void RemoveEmptyBook(VctBookInfo& vBookList); };DynPrgBuy.cpp
#include "StdAfx.h" #include "DynPrgBuy.h" #include <algorithm> using namespace std; CDynPrgBuy::CDynPrgBuy(void) { } CDynPrgBuy::~CDynPrgBuy(void) { } void CDynPrgBuy::BuyBook(const VctBookInfo& vBookList, VctBuyPlan& vBuyPlan) { BuyByDyn(vBookList, vBuyPlan); } //动态规划求最优的问题 double CDynPrgBuy::BuyByDyn(VctBookInfo vBookList, VctBuyPlan& vBuyPlan) { RemoveEmptyBook(vBookList); if (vBookList.empty()) { return 0; } //对书本个数从大到小排列 sort(vBookList.begin(), vBookList.end(), greater<StBookInfo>()); static int s_count = 0; ++s_count; cout<<s_count<<endl; map<double, VctBuyPlan> mapBuyPlan; int nBookCount = (int)vBookList.size(); for (int i = nBookCount; i >=1; i--) { int nBuyCount = i; VctBookInfo vTempList = vBookList; //计算当前所有的折扣 double dbOff = 0; switch (nBuyCount) { case 1: dbOff = 0; break; case 2: dbOff = 0.05; break; case 3: dbOff = 0.10; break; case 4: dbOff = 0.20; break; case 5: dbOff = 0.25; break; } StBuyPlan buyPlan; buyPlan.dbOff = dbOff; for (int j = 0; j < nBuyCount; j++) { StBookInfo& bkInfo = vTempList[j]; --bkInfo.nBookCount; buyPlan.vBuyBooks.push_back(bkInfo.strVolIdx); } //计算左折扣 double dbLeftOff = dbOff*nBuyCount; //计算右折扣,需递归 VctBuyPlan vTempPlan; vTempPlan.push_back(buyPlan); double dbRightOff = BuyByDyn(vTempList, vTempPlan); //当前购买方式折扣 double dbCurOff = dbLeftOff + dbRightOff; //折扣与组合方式映射 mapBuyPlan.insert(make_pair(dbCurOff, vTempPlan)); } if (mapBuyPlan.empty()) { return 0; } //取所有折扣中的最大值 //利用map特性取最后一个值即为最大值 map<double, VctBuyPlan>::iterator mBigPos = mapBuyPlan.end(); --mBigPos; vBuyPlan.insert(vBuyPlan.end(), mBigPos->second.begin(), mBigPos->second.end()); return mBigPos->first; } void CDynPrgBuy::RemoveEmptyBook(VctBookInfo& vBookList) { VctBookInfo::iterator vIter = vBookList.begin(); for (; vIter != vBookList.end();) { if (vIter->nBookCount > 0) { ++vIter; } else { vIter = vBookList.erase(vIter); } } }运行结果如下。
当前折扣:0.8=0.2*4 购买组合:卷2 卷3 卷4 卷1
当前折扣:0.8=0.2*4 购买组合:卷2 卷3 卷4 卷5
总的折扣:1.6
循环体执行了124次
由于采用了递归,时间复杂度太高,这是一个指数级的算法。
改进动态规划
仔细分析可以发现,递归算法中重复执行了很多步骤。
比如F(1,2,2,2,1)中包含F(1,1,1,0,0)求解,我们将这个值做下缓存。当再次需要F(1,1,1,0,0)值时,直接从缓存中获取即可。
DynPrgBuyM.h
#pragma once #include "IBuyMethod.h" //动态规划算法求解 class CDynPrgBuyM : public IBuyMethod { public: CDynPrgBuyM(void); ~CDynPrgBuyM(void); virtual void BuyBook(const VctBookInfo& vBookList, VctBuyPlan& vBuyPlan); private: double BuyByDyn(VctBookInfo vBookList, VctBuyPlan& vBuyPlan); void BuyAllGroup(const VctBookInfo& vBookList, map<double, VctBuyPlan>& mapBuyPlan); //移除已空的书籍 void RemoveEmptyBook(VctBookInfo& vBookList); CString GetBuyKey(const VctBookInfo& vBookList); private: map<CString, map<double, VctBuyPlan>> m_BuyMap;//加载一个缓存来提高计算效率 };DynPrgBuyM.cpp
#include "StdAfx.h" #include "DynPrgBuyM.h" #include <algorithm> using namespace std; CDynPrgBuyM::CDynPrgBuyM(void) { } CDynPrgBuyM::~CDynPrgBuyM(void) { } void CDynPrgBuyM::BuyBook(const VctBookInfo& vBookList, VctBuyPlan& vBuyPlan) { BuyByDyn(vBookList, vBuyPlan); } //动态规划求最优的问题 double CDynPrgBuyM::BuyByDyn(VctBookInfo vBookList, VctBuyPlan& vBuyPlan) { RemoveEmptyBook(vBookList); if (vBookList.empty()) { return 0; } //对书本个数从大到小排列 sort(vBookList.begin(), vBookList.end(), greater<StBookInfo>()); map<double, VctBuyPlan> mapBuyPlan; CString strBuyKey = GetBuyKey(vBookList); map<CString, map<double, VctBuyPlan>>::iterator mFindPos = m_BuyMap.find(strBuyKey); if (mFindPos != m_BuyMap.end()) { mapBuyPlan = mFindPos->second; } else { static int s_count = 0; ++s_count; cout<<s_count<<endl; BuyAllGroup(vBookList, mapBuyPlan); m_BuyMap.insert(make_pair(strBuyKey, mapBuyPlan)); } if (mapBuyPlan.empty()) { return 0; } //取所有折扣中的最大值 //利用map特性取最后一个值即为最大值 map<double, VctBuyPlan>::iterator mBigPos = mapBuyPlan.end(); --mBigPos; vBuyPlan.insert(vBuyPlan.end(), mBigPos->second.begin(), mBigPos->second.end()); return mBigPos->first; } void CDynPrgBuyM::BuyAllGroup(const VctBookInfo& vBookList, map<double, VctBuyPlan>& mapBuyPlan) { int nBookCount = (int)vBookList.size(); for (int i = nBookCount; i >=1; i--) { int nBuyCount = i; VctBookInfo vTempList = vBookList; //计算当前所有的折扣 double dbOff = 0; switch (nBuyCount) { case 1: dbOff = 0; break; case 2: dbOff = 0.05; break; case 3: dbOff = 0.10; break; case 4: dbOff = 0.20; break; case 5: dbOff = 0.25; break; } StBuyPlan buyPlan; buyPlan.dbOff = dbOff; for (int j = 0; j < nBuyCount; j++) { StBookInfo& bkInfo = vTempList[j]; --bkInfo.nBookCount; buyPlan.vBuyBooks.push_back(bkInfo.strVolIdx); } //计算左折扣 double dbLeftOff = dbOff*nBuyCount; //计算右折扣,需递归 VctBuyPlan vTempPlan; vTempPlan.push_back(buyPlan); double dbRightOff = BuyByDyn(vTempList, vTempPlan); //当前购买方式折扣 double dbCurOff = dbLeftOff + dbRightOff; //折扣与组合方式映射 mapBuyPlan.insert(make_pair(dbCurOff, vTempPlan)); } } void CDynPrgBuyM::RemoveEmptyBook(VctBookInfo& vBookList) { VctBookInfo::iterator vIter = vBookList.begin(); for (; vIter != vBookList.end();) { if (vIter->nBookCount > 0) { ++vIter; } else { vIter = vBookList.erase(vIter); } } } CString CDynPrgBuyM::GetBuyKey(const VctBookInfo& vBookList) { CString strTemp = _T(""); CString strKey = _T(""); VctBookInfo::const_iterator vIter = vBookList.begin(); for (; vIter != vBookList.end(); ++vIter) { strTemp.Format(_T("%d"), vIter->nBookCount); strKey += strTemp; strKey += _T("$"); } return strKey; }
运行结果
当前折扣:0.8=0.2*4 购买组合:卷2 卷3 卷4 卷1
当前折扣:0.8=0.2*4 购买组合:卷2 卷3 卷4 卷5
总的折扣:1.6
循环体运行8次即可