描述
小Hi和小Ho在兑换到了喜欢的奖品之后,便继续起了他们的美国之行,思来想去,他们决定乘坐火车前往下一座城市——那座城市即将举行美食节!
但是不幸的是,小Hi和小Ho并没有能够买到很好的火车票——他们只能够乘坐最为破旧的火车进行他们的旅程。
不仅如此,因为美食节的吸引,许多人纷纷踏上了和小Hi小Ho一样的旅程,于是有相当多的人遭遇到了和小Hi小Ho一样的情况——这导致这辆车上的人非常非常的多,以至于都没有足够的位置能让每一个人都有地方坐下来。
小Hi和小Ho本着礼让他们的心情——当然还因为本来他们买的就是站票,老老实实的呆在两节车厢的结合处。他们本以为就能够这样安稳抵达目的地,但事与愿违,他们这节车厢的乘务员是一个强迫症,每隔一小会总是要清扫一次卫生,而时值深夜,大家都早已入睡,这种行为总是会惊醒一些人。而一旦相邻的一些乘客被惊醒了大多数的话,就会同乘务员吵起来,弄得大家都睡不好。
将这一切看在眼里的小Hi与小Ho决定利用他们的算法知识,来帮助这个有着强迫症的乘务员——在不与乘客吵起来的前提下尽可能多的清扫垃圾。
小Hi和小Ho所处的车厢可以被抽象成连成一列的N个位置,按顺序分别编号为1..N,每个位置上都有且仅有一名乘客在休息。同时每个位置上都有一些垃圾需要被清理,其中第i个位置的垃圾数量为Wi。乘务员可以选择其中一些位置进行清理,但是值得注意的是,一旦有编号连续的M个位置中有超过Q个的位置都在这一次清理中被选中的话(即这M个位置上的乘客有至少Q+1个被惊醒了),就会发生令人不愉快的口角。而小Hi和小Ho的任务是,计算选择哪些位置进行清理,在不发生口角的情况下,清扫尽可能多的垃圾。
提示一:无论是什么动态规划,都需要一个状态转移方程!
小Hi面对这个问题也是不慌不忙,反倒决定借此机会让小Ho学习一下状态压缩动态规划,于是推了推一旁仍然晕晕乎乎的小Ho,问道:“小Ho,你说这样的问题能否使用动态规划进行解决?”
小Ho思索了一番,在心中默默将这个问题与背包问题进行类比后,道:“我觉得似乎不可以,这个问题和背包问题其实是很类似的,不过背包问题对于选取物品的限制是总重量不能超过一定数额。但在这里却是要求不能够在连续的一段位置中选取太多,如果我仍然以编号从小到大划分阶段,单单以best(i)表示当前已经决定了编号为1..i的位置是否选取的情况下最多可以清扫的垃圾数量的话,因为我不知道之前具体的选取方案,我是没有办法判断当前这个位置能否进行选取的,这便是违反了动态规划状态定义的无后效性!”
小Hi点了点头表示赞同,但随即继续问道:”在背包问题中,正如我们之前经历时所说,我们是通过将best(i)变成best(i, j),增加一个量——当前已经选取物品的总重量j到状态中,从而能够判断当前是否能够继续选取物品,那么在这里,你觉得我们需要添加什么样的量才能够达成我们的目的呢?”
这个问题顿时难倒了小Ho,但他也不是轻易放弃的性格,便拿出纸笔开始写写画画:“正如我之前所说,我不知道之前具体的选取方案,我是肯定没有办法判断当前这个位置能否进行选取的!那么我需要做的事情无非就是在状态中记录之前的选取方法,并且这些记录需要能够让我推算出当前这个位置的垃圾是否能被清扫而不引起口角!”
思路一旦清晰,各种想法便接踵而至,小Ho思索片刻便得出了结论:“如果我将之前每一个位置是否选取的信息都存储下来的话,那么到了决定最后一个位置的时候,最坏情况下我就有2^(N-1)种可能的状态,这个是我所不能接受的,但是我真的需要这么多的信息么?”
“不需要!”小Ho说道:“我只需要知道我之前的M-1个位置中选取了多少个位置就可以了!如果这个数目小于Q,那么我当前就有两种决策方案——选与不选,不然就就只有不选这一种方案。”
小Hi听闻此言,皱了皱眉头,问道:“那你的状态难道就要定义成best(i, j)表示当前已经决定了编号为1..i的位置,并且从i-M+1 ... i-1这M-1个位置中已经选取了j个位置的情况下最多可以清扫的垃圾数量么?”
小Ho刚想称是,却想道小Hi不会无缘无故的问这种问题,于是仔细考虑,顿时发现其中不对:“状态固然是可以了,但是却没有办法进行转移,best(i, j)的下一步肯定是某个best(i+1, k),但是因为无法知晓i-M+1这个位置究竟是否在j个选取的位置中,所以是根本没有办法计算k的!”
“而一旦记录了i-M+1这个位置是否选取了的话,我就还需要记录i-M+2这个位置——因为在best(i+1, k)中它便是(i+1)-M+1的这个位置,以此类推,也就是说我不能够光记录从i-M+1 ... i-1 这M-1个位置中选取了多少个位置,我还要将具体选择了哪些位置都一一记录下来!”小Ho思考道:“那我便只有如此定义状态了——以best(i, p1, p2, p3, ... , pM-1)表示当前已经决定了编号为1..i的位置,并且第(i-j+1)个位置是否选取用pj进行记录(0表示未选取,1表示选取)的情况下最多可以清扫的垃圾数量!”
听完小Ho新的想法,小Hi终于点了点头,但也没放弃继续考校小Ho:“那你准备如何转移状态?”
“这个简单,我只需要统计p1..pM-1之和S——即选取的位置总数,并且根据这个数目进行决策!”小Ho说罢在纸上写出一个公式。
“那具体的计算顺序呢?”小Hi也是将每个步骤都问的详详细细的。
小Ho张口便道:“这个容易,因为每次转移都是从i向i+1进行的,所以我只需要按照i从小到大的顺序进行计算就可以了!”
提示二:好像什么不对劲?状态压缩哪里去了?
在得出结论之后,小Ho便拿出笔记本开始写程序,写着写着便注意到:“这个M是根据输入来的,那么我怎么开数组呢!难道要使用一些动态的方法?这样也未免太过复杂了吧,更何况即使我动态的开了数组,我也没有很好的方法来枚举这些位置,难道要写一大串的条件分支语句?”
思索无奈之下,只能够去询问小Hi,小Hi仿佛早就预料到了这个情况,掏出一张草稿纸来,写下了一个长度为5的01串10101,问道:“你看这是什么?”
“一个2进制串?我算算……等于21?”小Ho耿直的算了出来。
小Hi笑了笑“如果我说这便是M=6的情况下,以第一个01来表示你状态中的p1,第二个01来表示你状态中的p2,并依次类推,那么我是否可以用(i, 21)来表示你的(i, 1, 0, 1, 0, 1)这样一个状态呢?”
小Ho顿时恍然大悟:“是了!既然是01串,那么我就将这M-1个01视作一个二进制数又有何不可!这样一来,我的状态和状态转移方程岂非可以这样定义?”
“是的!这便是所说的状态压缩,它在处理一些变长/变维度的状态时时非常有效的,同时也可以利用位运算来优化代码,方便计算!”小Hi适时的做了总结。
“嗯嗯!我这便去写~”
输入
每个测试点(输入文件)有且仅有一组测试数据。
每组测试数据的第一行为三个正整数N、M和Q,意义如前文所述。
每组测试数据的第二行为N个整数,分别为W1到WN,代表每一个位置上的垃圾数目。
对于100%的数据,满足N<=1000, 2<=M<=10,1<=Q<=M, Wi<=100
输出
对于每组测试数据,输出一个整数Ans,表示在不发生口角的情况下,乘务员最多可以清扫的垃圾数目。
5 2 1 36 9 80 69 85Sample Output
201
题意:给定n个各个位置的垃圾数量,要求在连续m个座位中,清洁的座位不超过q个,求能清洁的最多垃圾数。
思路:动态规划思想之一是无后效性,而此题中限制了位置选取的数量,当位于第i个位置时,选or不选,按照一般动态规划思想,需要根据前面已选的座位数列出状态转移方程式,然而我们不可能每种情况都列出,所以,需要用到状态压缩。
- 枚举可行状态。将在m个座位数量的限制下,枚举每一种可能的状态,可行状态为2^m-1种(一开始我认为是2^m种,后来发现,我认为的这种情况是将q==m情况考虑了进来,而实际题目是不能够这样的,因为毫无推导意义,直接计算总和就好),并将可行状态保存下来。
- 列状态转移方程式。二维数组的行是每个位置的标号,列是每种可行状态,按照座位号从小到大递推。在第i个位置k状态下的最大垃圾清理数量,需要从前一个位置i-1中获得或者说前m-1个位置中获得,所以我们只需要比较出
的较大值即可,i-m+1位置处存储第1个决策,i位置处存储第m个决策,因为我们不知道是做了第i个位置的决策后的价值大(注意,决策里包括了清理or不清理),还是第i-m+1个位置清理后的价值大,所以需要进行比较,比如1,2,3,4,5,6,假设 i=6,m=5,在做了第i=6个位置的决策后,位置的状态是2,3,4,5,6,而未做第i=6个位置的决策前,位置的状态是1,2,3,4,5,所以需要将不同位置状态下的价值进行比较。dp[第i-1个位置][k状态去掉i位置决策后的状态],dp[第i-1个位置][k状态去掉i位置决策后状态+第i-m+1选定的状态]
单独解释为什么第i个位置不一定清理,因为我们只需要从1到i的最大垃圾清理数量,所以第i个位置是否清理并不重要,比如清理了第i个位置的垃圾,而且刚好是决策数刚好达到m,如果i+1位置处的垃圾数远大于i处的垃圾数,而又因为决策数已经达到了最大m无法再清理i+1处的垃圾,这种情况,并不需要清理i处的垃圾 - 遍历状态输出结果。在n处是最后的递推位置,由于1-n之间不同状态的价值不同,所以需要比较出最大值。
#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std;
#define N 10010
#define inf 0x3f3f3f3f
int dp[N][1025],vis[1025],ans[1025],value[N];
int n,m,q;
int b[11] = {1,2,4,8,16,32,64,128,256,512,1024};//m进制下最左边位置已选的状态
int top;
int Judge(int x)
{
int count = 0;
while(x)
{
if(x%2)
count ++;
x = x>>1;
}
return count;
}
void Init()
{
int length;
top = 0;
memset(vis,inf,sizeof(vis));
for(int i = 0; i < (1<<m);i++)
{
length = Judge(i);//计算i排列状态下,1的个数总和
if( length <= q)
vis[i] = length;//vis存储i状态下1的个数
}
for(int i = 0; i < (1<<m);i++)
{
if(vis[i] <= q)
ans[++top] = i;//在1不超过q的情况下将i状态存储
}
return;
}
int main()
{
int sum;
while(scanf("%d%d%d",&n,&m,&q)!=EOF)
{
memset(dp,0,sizeof(dp));
Init();
for(int i = 1; i <= n; i ++)
scanf("%d",&value[i]);
for(int i = 1; i <= n; i ++)//从m位置开始枚举到n位置
{
for(int k = 1; k <= top; k ++)//枚举top个状态下的最大价值
{
//找到在第i个位置ans[k]状态下的最大价值
dp[i][ans[k]] = max(dp[i-1][ans[k]>>1],dp[i-1][(ans[k]>>1)+b[m-1]])+(ans[k]&1)*value[i];
//在第i-1个位置处,去掉第i个位置决策的状态的价值(没有加入i-m+1的决策状态)
//在第i-1个位置处, 去掉第i个位置决策的状态+第i-m+1个位置为1即清理的状态的价值(加入i-m+1位置已选的状态)
//是否清理i位置处,是由ans[k]状态下的末位数决定,因为末位为1时表示清理i位置
}
}
sum = 0;
for(int i = 1; i <= top; i ++)
sum = max(sum,dp[n][ans[i]]);//n是最终位置,由于不同状态价值不同,所以需要取最大值
printf("%d\n",sum);//好歹理解完了,万岁
}
return 0;
}