poj-1505 Copying Books ***

/*
* 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个

posted on 2011-07-28 17:56  龙豆  阅读(473)  评论(0编辑  收藏  举报

导航