源于生活
物流问题向来都是采用线性规划,但是单一的线性规划往往只是简单地枚举,这是数学建模时最不愿意发生的。
今年的苏北赛C题:整车物流调度系统,数据量大,若要是以简单的相性规划来做显然是不现实的,我们以此题作为选拔题目,大部分队伍用各种软件、C/C++等程序来跑,效果极为不理想,时间开销非常大,多数都是2个小时以上,且各个队伍的结果相差较大。这就说明这种算法不易于实际操作,拼得是计算机硬件,而不是设计了。
还有一部分人喜欢用类似模拟退火、遗传算法等一些“靠运气”的算法,结果是能算出来,但是对于结果的好坏无法给与准确的评定,理论支持不甚完整。
基于上述情况,在当时做题时我们给出一个新的模型,对于类似问题简化非常有益。
高于生活
首先,我们先来分析一下物流问题的难点:
一、始发地有一批货物,一些运送的车辆,但是每辆车都有载重量的限制。在过路费、损耗费等相差不大(具体信息可参考2013苏北赛C题)的情况下。考虑空载费,本身对于货物的分配就存在困难,因为每辆车只能用一次,你不能保证每辆车都运送最合适的物资。
二、如果有一群始发地往一群接收地发送货物,你在建好最短路径的单行图后,对每辆车安排路线也会存在问题。因为考虑到一辆车可以沿途运送,对于路线的安排也将是非常困难。
三、从外地来的运货车在卸下本地所需的货物后,可以作为本地的运货车(时间卡的较松)。
根据这些问题,我先提出一个类似聚类的方法:城市群的概念:
一、如果某些城市的互通距离满足一定的限制,我们就可以将这些城市分配在一起形成城市群。
二、如果一个城市依靠有一个城市群,当它自身的订单运量较少时,便可以借助城市群里的其他城市来帮忙代运。
三、城市群之间可以以抽象车队形式进行大方向运送。
抽象发散
有一集合M,可以将M分成若干子集,且每个子集的数据个数不得超过n,每个子集中数据的总和不得超过sum(sum大于M中的每个元素,请在程序中自行检验),
对每种分配情况,若所有子集内容相同,但子集顺序不同的算不同情况,例如{{2,1},{1}} 与{{1},{2,1}}算不同情况;
对于每个子集中的数据顺序不同的算一种情况,例如一个子集{1,2}={2,1};
给出对M集合所有的分配情况,注意M中元素可以重复(已知条件为M,n,sum)。
对每种分配情况,若所有子集内容相同,但子集顺序不同的算不同情况,例如{{2,1},{1}} 与{{1},{2,1}}算不同情况;
对于每个子集中的数据顺序不同的算一种情况,例如一个子集{1,2}={2,1};
给出对M集合所有的分配情况,注意M中元素可以重复(已知条件为M,n,sum)。
具体事例:
M={1} ,n=3,sum无限制时:
1 — { {1} }
M={1,2} ,n=3,sum无限制时:
1 — { {2},{1} }
2 — { {1},{2} }
3 — { {1,2} }
M={1,2,3},n=3,sum无限制时:
1 — { {3},{2},{1} }
2 — { {3},{1},{2} }
3 — { {3},{1,2} }
……
10 — { {2,3},{1} }
11 — { {1,3},{2} }
12 — { {1,2},{3} }
13 — { {1,2,3} }
1 — { {1} }
M={1,2} ,n=3,sum无限制时:
1 — { {2},{1} }
2 — { {1},{2} }
3 — { {1,2} }
M={1,2,3},n=3,sum无限制时:
1 — { {3},{2},{1} }
2 — { {3},{1},{2} }
3 — { {3},{1,2} }
……
10 — { {2,3},{1} }
11 — { {1,3},{2} }
12 — { {1,2},{3} }
13 — { {1,2,3} }
模型应用:
一、对已知的订单集合进行装车分配:设定M,设定sum(最大车载量限制)。
二、对运输车辆进行路线的分配:设定M(是在城市群分配好的基础上),设定n(一辆车最多可以运送的城市数)。
分析:
1、注意到子集是有顺序的
可以保证货物分配时对车的优先选择;
可以保证分配路线时车对目标城市的优先选择。
2、子集的元素是没有顺序的
这样保证了车在装一批货,不关心货物的装车顺序;
对于货车的要运送的目标城市,根据实际订单一般就能选择路径(先送量大的),没有排序的必要。
可以保证货物分配时对车的优先选择;
可以保证分配路线时车对目标城市的优先选择。
2、子集的元素是没有顺序的
这样保证了车在装一批货,不关心货物的装车顺序;
对于货车的要运送的目标城市,根据实际订单一般就能选择路径(先送量大的),没有排序的必要。
优劣分析:
优势:
1、用一种目的性较强的选择方案极大地减少了选车和路径分配的情况;
2、其中的参数可以人为控制,方便对不同情况提供合理的调整(n,sum可极大限制对M的拆分情况);
3、采用分区分治的思想,符合实际生活中物流产业对订单的分配。
2、其中的参数可以人为控制,方便对不同情况提供合理的调整(n,sum可极大限制对M的拆分情况);
3、采用分区分治的思想,符合实际生活中物流产业对订单的分配。
劣势:
1、参数选择较麻烦;
2、如果M较大,则会耗时耗力,性能受影响(但是可以通过对sum限制减少时间开销,效果明显)。
推广:
1、对大部分物流问题求解都可以此计算。
2、划分出的城市群大小可变,也可对划分好的城市群递归继续划分,便于操作。
3、划分集合M时,通过对n和sum参数的调控可以极大减少分配情况,所以对大的集合也有很好的运行能力!
闲话说效果
对于本次我们做的苏北赛C题(针对第一小问),程序部分,其他队伍快则2小时,满则4小时,还有部分未给出结果,但是我们以此将性能提升为26秒(依个人电脑而定,但绝不会超过60s)。
思想较为新颖,因为当时我们未从网上得到有效信息(比赛结果没出来),绝对独家不重复。
主要代码
/*
创新函数,一种集合all的分配组合,
可以设置每个子集含数据个数max_group,目前最大为8(54 5835 - 78s)
可以设置每个子集所有数据的总和
*/
vector< vector< vector<int> > > all_combine(vector<int> all,
int max_group,
int max_sum)
{
//首先遍历检验条件
vector<int>::iterator iter_all;
for(iter_all=all.begin();iter_all!=all.end();iter_all++)
{
if(*iter_all > max_sum)
{
cerr<<"不符合分配条件!"<<endl;
exit(1);
}
}
vector< vector<vector <int> > > result;
int number_size=all.size();
//递归终点
if(number_size == 1)
{
vector< vector<int> > temp;
temp.push_back(all);
result.push_back(temp);
return result;
}
int i=0;
//设置每个子集中最大包含的数据个数
int max=number_size<=max_group?number_size:max_group;
for(i=1;i<=max;i++)
{
vector< vector<int> > arrange;
int a[100], b[100];
for(int j=0;j<number_size;j++)
a[j]=j+1;
arrange.clear();
combine(a, number_size, i, b, i,arrange);
vector< vector<int> >::iterator iter_arrange;
for(iter_arrange=arrange.begin();iter_arrange!=arrange.end();iter_arrange++)
{
//检验头部集合的和是否满足条件
vector<int>::iterator iter_int;
int sum=0;
for(iter_int=iter_arrange->begin();iter_int!=iter_arrange->end();iter_int++)
{
//注意位置编号是从0起!
sum+=all[*iter_int-1];
}
if(sum > max_sum)continue;
//递归用到,存放尾部——剔除已经排列好的部分
vector<int> temp;
//存放头部——已经排列好的部分
vector<int> temp_pro;
//生成头部和尾部
int pos=1;
vector<int>::iterator iter_all;
for(iter_all=all.begin(),pos=1;iter_all!=all.end();iter_all++,pos++)
{
vector<int>::iterator iter_int;
for(iter_int=iter_arrange->begin();iter_int!=iter_arrange->end();iter_int++)
{
// cout<<*iter_int<<" ";
if(*iter_int == pos)
{
temp_pro.push_back(*iter_all);
break;
}
}
//说明arrange的一组中没有此数,添加到尾部
if(iter_int == iter_arrange->end())
temp.push_back(*iter_all);
}
//递归算出尾部情况
vector< vector< vector<int> > > result_temp=all_combine(temp,max_group,max_sum);
//自身情况
if(result_temp.size() == 0)
{
vector< vector<int> > temp;
temp.push_back(temp_pro);
result.push_back(temp);
}
//将头部和所有尾部结合
vector< vector< vector<int> > >::iterator iter_resultemp;
for(iter_resultemp=result_temp.begin();iter_resultemp!=result_temp.end();iter_resultemp++)
{
vector< vector<int> > temp;
//不是指上述的容器的迭代器
vector< vector<int> >::iterator iter_temp;
temp.push_back(temp_pro);
for(iter_temp=iter_resultemp->begin();iter_temp!=iter_resultemp->end();iter_temp++)
{
temp.push_back(*iter_temp);
}
result.push_back(temp);
}
}
}
return result;
}
依赖(单一的组合):
//排列组合(无次序),n表示a的前n个数的组合
void combine(int a[], int n, int m, int b[], int M,vector< vector<int> > &arrange)
{
int i, j;
for (i = n; i >= m; i--)
{
b[m - 1] = i - 1;
if (m > 1)
combine(a, i - 1, m - 1, b, M,arrange);
else
{
vector<int> temp;
for (j = M - 1; j >= 0; j--)
{
temp.push_back(a[b[j]]);
}
arrange.push_back(temp);
}
}
}
如果有不解或是疑问欢迎讨论。