/*
* DP
* d[i][j] : 前i个人,分完1~j本书,d值满足:minimize the maximum number of pages assigned to a single scriber
* d[k][m]即为所求
*
* 状态转移方程:d[i][j] = min( max(d[i-1][t] , page[j] - page[t]) ) 其中: i-1 <= t <= j-1 (注意每个人至少一本书)
* 其中 page[i] 为 1~i本书的总页数, page[j]-page[t] 即为第j个人分到的页数
*
* 最后注意满足 “If there is more than one solution,
* print the one that minimizes the work assigned to the first scriber,
* then to the second scriber etc. ”
*
* 其实dp本身不难,这个才有点费劲~
*
* 开始用了int型数组record记录划分状态,后来TLE , 后来改成bool型, WA 以下数据出了错误:
* 5 3
* 1 1 1 1 10
* 答案是: 1 / 1 1 1 / 10
*
* 最后改成中间过程不记录, 直接算出答案, 再根据答案用贪心法逆向找出划分方法
*
*/
#include <cstdio>
#include <cstring>
using namespace std;
const int inf = 1000000000;
const int maxM = 500 + 5;
int n, m, k, p[maxM];
int d[maxM][maxM], page[maxM];
//bool record[2][maxM][maxM]; record[][x][y]=1 表示: 分完1~x本书的情况中,第y本书后应该加‘/’。
//。根据状态转移方程,只需记录两组数据
int inline get_max(int lhs, int rhs){
return (lhs > rhs ? lhs : rhs);
}
int main(){
scanf("%d", &n);
while(n--){
scanf("%d %d", &m, &k);
memset(d, 0, sizeof(d));
memset(page, 0, sizeof(page));
// memset(record, 0, sizeof(record));
int last = 1, cur = 0;
for(int i=1; i<=m; i++){
scanf("%d", &p[i]);
page[i] = page[i-1] + p[i];
d[1][i] = page[i];
// record[cur][i][i] = 1;
}
for(int i=2; i<=k; i++){
int tmp = cur; cur = last; last = tmp;
for(int j=i; j<=m-k+i; j++){
d[i][j] = inf;
for(int t=i-1; t<=j-1; t++){
int tmpMax = get_max(d[i-1][t], page[j]-page[t]);
if(d[i][j] > tmpMax){
d[i][j] = tmpMax;
// memcpy(record[cur][j], record[last][t], sizeof(bool)*(t+1));
// record[cur][j][j] = 1;
}
}
}
}
//逆向找出划分方法
bool record[maxM] = {};
int tmpSum = 0, slashNum = 0;
for(int i=m; i>=1; i--){
if(tmpSum + p[i] > d[k][m]){
record[i] = 1; slashNum++;
tmpSum = p[i];
}
else tmpSum += p[i];
}
if(slashNum < k-1){ //把slash补足
for(int i=1; i<=m && slashNum!=k-1; i++){
if(record[i] != 1){
record[i] = 1; slashNum++;
}
}
}
for(int i=1; i<m; i++){
printf("%d", p[i]);
if(record[i]) printf(" / ");
else printf(" ");
}
printf("%d\n", p[m]);
}
return 0;
}
——————————————————————————————————————————————————————————
在网上又找到 二分查找+判定 的方法:
//二分查找+判定 (思想很经典)
#include <cstdio>
#include <cstring>
typedef __int64 llong;
const int MAXN = 510;
llong book[MAXN];
bool use[MAXN];
int N, K;
llong Max(llong a, llong b){return a > b ? a : b;}
int check(llong L)
{
int i, cnt;
llong sum = 0;
i = N - 1;
//用来标记段数,对于不同的L值,都是先进行更新。
memset(use, 0, sizeof(use));
//段数
cnt = 1;
while (i >= 0)
{
//大于,则须在I处断开,即任何一段之和要小于L
if (sum + book[i] > L)
{
//标记此处要段开
use[i+1] = 1;
//段数加1
cnt++;
//开始新的一段
sum = book[i];
}
else
{
//小于,仍属于此段
sum += book[i];
}
i--;
}
return cnt;
}
void solve()
{
int i;
llong min, max, mid, cnt, sum;
min = 0;
sum = 0;
scanf("%d %d", &N, &K);
//二分的起始点为[最小的页数 ,页数之和],因为最大值一定在这之间
for (i = 0; i < N; i++)
{
scanf("%I64d", &book[i]);
sum += book[i];
min = Max(min, book[i]);
}
max = sum;
//二分查找
while (min < max)
{
mid = (min + max) / 2;
//如果以MID为最大值,而得到的段数小于等于K,说明MID值太大了
if (check(mid) <= K)
max = mid;
//否则MID值太小,使得段数大于K
else
min = mid + 1;
}
//求出以MAX为最大值所能够得到的段数。(从后至前,因为题目要求使得前面的任务越小越好)
cnt = check(max);
for (i = 1; i < N && cnt < K; i++)
{
//多余的段数全部用在最前面,使得前面的工人任务数是最优解中最少的
if (!use[i])
{
use[i] = true;
cnt++;
}
}
for (i = 0; i < N; i++)
{
printf("%I64d ", book[i]);
if (use[i+1])
printf("/ ");
}
printf("\n");
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
solve();
return 0;
}
——————————————————————————————————————————————————————————————————————————————
附:discuss :
二分+贪心需要注意的几个地方:
贪心:题目要求划分的区间编号字典序最小,因此需要从右向左贪心,若当前区间和>二分枚举值maxs 则区间数+1
判断可行性时,
1.if book[i]>maxs return false
2.只要是需要的区间个数<=m 即return true
二分结束后,
1.再执行一次judge过程,以便pos数组保存的是最终的结果
2.从小到大,将区间个数补足m个