【算法 | 实验8】分配最小页数(数组划分和最大值最小化问题)

简介: 【算法 | 实验8】分配最小页数(数组划分和最大值最小化问题)

题目


分配最小页数 (学校OJ 357题)

给定n本书的页数,m名学生。这些书按页数的升序排列成一个序列。每个学生都被分

配去读一些序列中连续的书。设分配给第i个学生的书籍的总页数为Pi,1<=i<=m。找出

优化的分配方式,使得最大的Pi最小。用分治法求解。

输入描述

第一行输入t,表示t个测试用例

对于每一个测试用例第一行输入n和m,表示书的数量和学生的数量

第二行按升序输入n个整数,表示书的页数。

输出描述

t行,表示t个测试用例的解

样例输入

2

4 2

12 34 6790

15 7

12 34 67 90 95 103 150 165 170 198 201 235 240 245 251

样例输出

113

436


问题分析与算法设计思路

思路1:类似枚举的分治(暴力)

基本思路:我们这样划分问题,不妨使第一个同学看书 x 页;而剩余的书和同学则构成一个原问题的子问题,使剩余同学中看书最多的同学看了 y 页。那么所有同学中的最大页数z = m a x ( x , y ) z = max(x,y)z=max(x,y)。

而对于剩余的书和同学,我们继续划分为一个同学和其它剩余的同学。直到只剩下一个同学时,就不可再分,组合这些子问题的解将得到最初原问题的解。


思路2:二分法

在思路1中,我们以同学为主要对象进行,通过改变给同学分配的书的数量,来搜索解空间。


基本思路:这里对问题引入一种新的描述,以方便理解。我们把每个同学看作一个桶,而每本书看作一个物品,书的页数就是物品的大小。每个桶有一个最大容量,分配书籍就是将物品一个个放入桶中,当一个桶放不下时,就将物品放到下一个桶中。那么问题就变成了,要找一个最小桶容量,而恰好能将所有的物品都放进桶中。


因此,这里问题的主要对象是桶的容量。


算法过程:对桶容量进行二分搜索,找到最小桶容量。


首先确定一个桶容量的大致范围。不妨令第 i 个物品的大小为 a [ i ] a[i]a[i],所有物品大小和为 sum,则桶容量的初始区间为 [ a [ m ] , s u m ] [a[m],sum][a[m],sum](m 为物品数量)。


搜索:

设初始搜索区间为 [ f i r s t , e n d ] [first,end][first,end]


先取中间值 m i d = ( f i r s t + e n d ) / 2 mid=(first+end)/2mid=(first+end)/2,然后对容量 mid 进行判断

恰好合适,可以放下所有物品,但桶容量再减小 1 就放不下了。

桶小了,放不下所有物品,则搜索区间的右边 [ m i d + 1 , e n d ] [mid+1,end][mid+1,end]

桶大了,能放下所有物品,但不是恰好合适,则搜索区间左边 [ f i s r t , m i d − 1 ] [fisrt,mid-1][fisrt,mid−1]。

递归求解。

判断桶能否放下所有物品:


遍历物品序列,逐个放入桶中。若需要桶的数量大于 m,则桶容量小了。

算法实现(C++)

思路1实现

#include<iostream>
using namespace std;
const int Big = 8848;//用作极大值 
int renshu;//学生人数 
int anser(int m,int n,int l,int r,int a[])//表示m堆书,n个学生在分摊之后,返回学生所看书的最大页数 
{
//  cout<<"m="<<m<<" n="<<n<<" l="<<l<<endl;
  int answer = 0;
  if(n == 1)
  {
  int sum = 0;
  for(int i = l; i <= r; i++)
  {
    sum += a[i];
  }
  answer = sum;
  goto label;
  }
  if(m == n)
  {
  answer = a[r];
  goto label;
  }
  if(m < n)
  {
  answer = Big;
  goto label;
  }
  if(m > n)
  {
  int minOfMax = Big;
  int sum = 0;
  for(int i = l; i <= m - n + l; i++)
  {
//    cout<<"-------------------i="<<i<<"  m-n+l= "<<m-n+l<<endl;
    sum += a[i];
    int t = anser(m-i+l-1, n-1, i+1, r, a);
    int maxNum = max(sum, t);//原问题分成两部分,取最大值 
    if(maxNum == 13)
    {
    system("pause");
    }
    minOfMax = min(minOfMax, maxNum);
  }
  answer = minOfMax;
  }
label:
  return answer;
}
int main()
{
  int t,m,n;
  cin>>t;//t表示测试案例数 
  for(int j=0;j<t;j++)
  {
  cin>>m>>n;//m表示书堆的数目,n表示学生数目 
  renshu=n;
  int *a=new int[m+1];
  for(int i=0;i<m;i++)
    cin>>a[i];  
  int answer = anser(m,n,0,m-1,a);//递归求解 
  cout<<"answer: "<<answer<<endl;
  delete [] a;
  }
  return 0;
} 
/*测试数据
2
4 2
12 34 67 90
15 7
12 34 67 90 95 103 150 165 170 198 201 235 240 245 251
*/

思路2实现

运行


image.png


代码

为了方便展示运行效果,代码中嵌入了一些输出中间变量值的语句。而且在 main 函数中仅安排了一个测试用例的输入。

#include<iostream>
using namespace std;
int TwoPointSearch(int f, int e, int a[], int n, int m);
bool Can(int a[], int n, int x, int m);
int main(){
  int n = 0;//物品数量
  int m = 0;//桶的数量 
  int sum = 0;//物品大小之和 
  cin >> n >> m;
  int *a = new int[n];
  for(int i = 0; i < n; i++)
  {
  cin >> a[i];
  sum += a[i];
  }
  int answer = TwoPointSearch(a[n-1], sum, a, n, m);
  cout<<"answer:"<<answer<<endl; 
  return 0;
}
int TwoPointSearch(int f, int e, int a[], int n, int m)
{
  //返回:能装下的最小桶容量
  cout << "arange:" << f << " " << e;
  int mid = (f + e) / 2;
  bool can = Can(a, n, mid, m);
  cout << "  can:" << can;
  cout << "  mid:" << mid << endl;
  if(can)
  {
  int can_smaller = Can(a, n, mid-1, m);
  if(! can_smaller)
  {
    return mid;
  }
  else
  {
    return TwoPointSearch(f, mid-1, a, n, m);
  }
  }
  else
  {
  return TwoPointSearch(mid+1, e, a, n, m);
  }
}
bool Can(int a[], int n, int x, int m)
{
  //放得下吗?
  //n:物品数,m:桶数 
  int flag = false; //to test 
  if(x < a[0])
  {
  return false;
  }
  int need = 0;//需要桶的数量
  int sum = 0;//一个桶已经装了多少 
  for(int i = 0; i < n; i++)
  {
  if(sum + a[i] <= x)
  {
    sum += a[i];
  }
  else
  {
    sum = a[i];
    need++;
  }
  if(need + 1 > m)
  {
    return false;
  }
  } 
  return true;
}

bug记录

1、子问题对最大值没有实现最小化

问题分析

answer这个全局变量很不好,破坏了递归的逻辑。


本来要分治,前面一部分就是k,然后后面是剩余部分的最小化最大值。


代码里是用什么实现最大值的最小化的?就是answer,但answer之会在递归的第一层更新。


也就是说,对分治后的后面一段子数组,并没有实现最大值的最小化。


解决方案(见思路1实现)

改用将 answer 作为函数内的局部变量的写法。


问题代码版本


#include<iostream>
using namespace std;
int answer=100000;
int maxsize=1;
int renshu;
int anser(int m,int n,int l,int r,int a[])//表示m堆书,n个学生在分摊之后,返回学生所看书的最大页数 
{
//  cout<<"m="<<m<<" n="<<n<<" l="<<l<<endl;
  if(n==1)//如果只有一个学生,就直接返回a数组中的值之和 
  {
  int sum=0;
  for(int i=l;i<=r;i++)
  {
    sum+=a[i];
  }
  return sum;
  }
  else if(m==n)//若书堆数和学生数相同,因为要保证每个学生都有书看,而且数组a已经排好序了,所以直接返回最后一个数即可 
  {
  return a[r];
  }
  else if(m<n)//此种情况会导致有学生没有书看,所以返回零(这个不大确定,我是乱写的) 
  return 0;
  else if(m>n)//当书堆数大于学生人数时 
  {
  int k=0;
  for(int i=l;i<=m-n+l;i++)//表示列举可能的组合 ...这里不知道咋解释 
  {
    k+=a[i];
    maxsize=max(k,anser(m-i+l-1,n-1,i+1,r,a));
    if(n==renshu)
    {
    answer=min(answer,maxsize);
    }
  }
  }
}
int main()
{
  int t,m,n;
  cin>>t;//t表示测试案例数 
  for(int j=0;j<t;j++)
  {
  cin>>m>>n;//m表示书堆的数目,n表示学生数目 
  renshu=n;
  int *a=new int[m+1];
  for(int i=0;i<m;i++)
  cin>>a[i];  
  anser(m,n,0,m-1,a);//递归求解 
  cout<<answer;
  delete [] a;
  answer=100000;
  }
  return 0;
 }

2、保存的最大值是局部的

问题分析

可以对比思路1实现。原来b也通过answer返回了,到上一层求b又有一次max的筛选,得到的才是最大值。

直接将b保存下来,只经过了与本层k的比较,就相当只比较了两个人的看书量,不是该分配情况下所有人看书最多的那一个。


b数组应该存的是若干个最大值,然后通过排序最小化最大值。现在你代码并没有保证存的b是个“最大值”


问题代码版本


#include<iostream>
#include<vector>
#include<cstdlib>
#include<algorithm>
using namespace std;
int renshu,book;
vector<int>v;
vector<int>::iterator it;
int maxsize=100000;
void anser(int m,int n,int l,int r,int a[],int &b)//表示m堆书,n个学生在分摊之后,返回学生所看书的最大页数 
{
  if(n==1)
  {
  int sum=0;
  for(int i=l;i<=r;i++)
  {
    sum+=a[i];
  }
  b=sum;
  return ;
  }
  else if(m==n)
  {
  b=a[r];
  return ;
  }
  else if(m<n)
  {
  b = 8848;
  return;
  }
  else if(m>n)
  {
  int k=0;
  for(int i=l;i<=m-n+l;i++)
  {
    if(n==1)
    break; 
    else if(m==n)
    break;
    k+=a[i];
    int temp;
    anser(m-i+l-1,n-1,i+1,r,a,temp);
    b=max(k,temp);
    v.push_back(b);
  }
  return;
  }
}
int main()
{
  int t,m,n,b=0,sum=0;
  cin>>t;//t表示测试案例数 
  for(int j=0;j<t;j++)
  {
  sum=0;
  cin>>m>>n;//m表示书堆的数目,n表示学生数目 
  book=m;
  renshu=n;
  int *a=new int[m+1];
  for(int i=0;i<m;i++)
  {
    cin>>a[i];
    sum+=a[i];
  }
  if(n==1)
  cout<<sum<<endl;
  else if(m==n)
  cout<<a[m-1]<<endl;
  else
  {
    anser(m,n,0,m-1,a,b);//递归求解 
    sort(v.begin(),v.end());
    cout<<*v.begin()<<endl;
  } 
  delete [] a;
  }
  return 0;
 }

运行结果


image.png

算法分析

思路1

本来枚举的时间复杂度应为 O ( 2 n ) \Omicron (2^n)O(2

n

) (n 为书数量),每本书都有分给当前同学和分给下一个同学两种选择。不过还有剪枝的优化。


思路2

采用二分搜索的时间复杂度为 O ( l o g ( m ) ) \Omicron (log(m))O(log(m)) (m 为桶数),但每次判断桶容量是否合适都是在遍历物品序列,遍历的时间复杂度为 O ( n ) \Omicron(n)O(n) (n 为物品数量)。


因此总的时间复杂度应为:

image.png


相关文章
|
27天前
|
算法
Leetcode 初级算法 --- 数组篇
Leetcode 初级算法 --- 数组篇
37 0
|
3月前
|
算法 测试技术
【算法】二分算法——寻找旋转排序数组中的最小值
【算法】二分算法——寻找旋转排序数组中的最小值
|
3月前
|
算法
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
|
17天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
39 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
20天前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
18 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
4天前
|
存储 缓存 算法
优化轮询算法以提高资源分配的效率
【10月更文挑战第13天】通过以上这些优化措施,可以在一定程度上提高轮询算法的资源分配效率,使其更好地适应不同的应用场景和需求。但需要注意的是,优化策略的选择和实施需要根据具体情况进行详细的分析和评估,以确保优化效果的最大化。
|
19天前
|
算法 搜索推荐 Java
java 后端 使用 Graphics2D 制作海报,画echarts图,带工具类,各种细节:如头像切割成圆形,文字换行算法(完美实验success),解决画上文字、图片后不清晰问题
这篇文章介绍了如何使用Java后端技术,结合Graphics2D和Echarts等工具,生成包含个性化信息和图表的海报,并提供了详细的代码实现和GitHub项目链接。
67 0
java 后端 使用 Graphics2D 制作海报,画echarts图,带工具类,各种细节:如头像切割成圆形,文字换行算法(完美实验success),解决画上文字、图片后不清晰问题
|
20天前
|
存储 算法 定位技术
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
这篇文章主要介绍了稀疏数组和队列的概念、应用实例以及如何使用数组模拟队列和环形队列的实现方法。
19 0
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
|
26天前
|
算法
计科一二班算法数据结构实验9答案
计科一二班算法数据结构实验9答案
13 0
|
3月前
|
算法 Java 测试技术
算法分析(蛮力法与减治算法应用实验报告)
这篇文章是关于算法分析的实验报告,介绍了如何使用蛮力法解决背包问题,并通过伪代码和Java代码实现,同时分析了其时间效率;还介绍了基于减治法思想实现的二叉查找树的插入与查找,同样提供了伪代码、Java源代码实现和时间效率分析,最后展示了测试结果截图。
算法分析(蛮力法与减治算法应用实验报告)