这是在codeforces上参加的第二场比赛了,虽然最后测评的结果显示在房间排名是第二名,但是与第一名的差距之大却是很难看的。这里,简单地报告一下E题Summer Reading的两种解法。比赛完后,看了作者提供的tutorial仍然是一脸雾水,主要是因为误解block为输入中连续的数字块。之后,借鉴了牛人的代码,才知道block其实是指一本书号的数字块。如给定输入序列为0 0 2 2 3 3 0 0 4 4 0,这其中就存在着三个block,即2 2;3 3;4 ;而非两个block(2 2 3 3;4 ;)。
解法一(思想源于作者的tutorial):
根据作者提供的DP思想,对于每一个block都可以尝试进行向左或向右扩展,如 4 可以向左扩展为 4 4 4 ,然后再向右扩展,就变为了 4 4 4 4 ,并且符号题中所给条件。对于一个特定的block[i]来说,其向左扩展的限制在于其左边的block[i-1]的向右扩展结果,block[i-1]与block[i]间缺少的书号,以及block[i-1]后面紧跟着的0的个数,而其向右扩展的限制在于其向左扩展的结果以及block[i]后面紧跟着的0的个数。
示例:0 0 2 2 3 3 0 0 4 0
block[1] = 2 2,此时已无法向左扩展(即向左扩展0位),因为必须block[1]之前缺少的书号的数目为1(即只有书号1缺失),至少要占据两个0。并且由于block[1]后面没有紧跟着的0,所以向右也无法扩展。
block[2] = 3 3,向左无法扩展,因为block[1]与block[2]之间没有0存在。向右可以扩展1位或者2位,都不会打破题中的限制。
block[3] = 4 4,向左扩展讨论:在block[2]扩展1位的情况下,block[3]向左扩展1位;在block[2]扩展2位的情况下,block[3]向左无扩展(即扩展0位)。向右扩展讨论:在block[3]向左扩展1位或者2位的情况下,block[3]向右扩展1位,或者不扩展。
在block[3]向左不扩展时,block[4]向右扩展1位。
在上述block扩展的过程中,如果有一个block不管是向右扩展几位(包括扩展0位)都无法满足题中条件时,就直接输出-1,并退出执行。否则,就可以利用block的扩展情况进行还原书号序列。
下面用两个数组exLeft和exRight来分别表示每个block的向左扩展情况和向右扩展情况。为了方便,令block[1]之前还存在着一个block[0]。
block[0]:为了不占用block[0]后面的零,令exRight[0][0]= true,exRight[0][1-4] = false
block[1]:exLeft[1][0] = true,exLeft[1][1-4]= false(block[1]无法向左扩展);同理,exRight[1][0]= true,exRight[1][1-4] = false。
block[2]:exLeft[2][0] = true,exLeft[2][1-4]= false;exRight[2][0-2] = true,exRight[2][3-4]= false(block[2]向右扩展1位或者2位均可)。
block[3]:exLeft[3][0-1] = true,exLeft[3][2-4]= false;exRight[3][0-1] = true,exRight[3][2-4]= false(exRight[3][0] = true对应于exLeft[3][1]= true,而exRight[3][1] = true对应于exLeft[3][0]= true或者exLeft[3][1] = true)。
对于最后一个block,为了使得留出更多的0来填充更大的书号,我们要使用exRight[3][i] = true最小的那个i,即i= 0,但由于此时只剩下一个0,无法完整填充更大的书号,所以i=1。接着寻找可以与exRight[3][1]= true相对应的exLeft[3][j],然后利用exLeft[3][j]找到可以与其相对应的exRight[2][k],如此递归下去。对于每个block,利用找到的合适的exLeft和exRight进行向左、向右扩展,完成书号的填充。
在编码过程中,exLeft[i][j]里面可以记录可以与其相对应的一个exRight[i-1][k]中的k值,这样就可以快速向前递归填充了。
代码:
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <utility>
#include <set>
#include <map>
#include <cstdio>
#include <cstdlib>
#include <cmath>
using namespace std;
class BLOCK {
public:
int elem;
int numElem;
int numZero;
public:
BLOCK(int _elem = 0, int _numElem = 0, int _numZero = 0)
{
elem = _elem;
numElem = _numElem;
numZero = _numZero;
}
};
int n;
vector<int> note;
vector<BLOCK> block;
vector<vector<int> > toLeft, toRight;
vector<int> ans;
bool Work()
{
block.clear();
block.push_back( BLOCK(0,0,0) );
for(int i = 0; i < n; ++i)
{
if( !note[i] )
{
block.back().numZero++;
}
else
{
if( note[i] != block.back().elem )
{
block.push_back( BLOCK(note[i], 1, 0) );
}
else
{
block.back().numElem += block.back().numZero + 1;
block.back().numZero = 0;
}
}
}
toLeft.assign(block.size(), vector<int>(5,-1));
toRight.assign(block.size(), vector<int>(5,-1));
toRight[0][0] = 0;
for(int i = 1; i < int(block.size()); ++i)
{
// calculate toLeft[i], which is only related to toRight[i-1]
for(int l = 0; l < 5; ++l)
{
if( l + block[i].numElem > 5 ) continue;
for(int r = 0; r < 5 && toLeft[i][l] < 0; ++r)
{
if( toRight[i-1][r] < 0 ) continue;
if( r + l <= block[i-1].numZero )
{
int zeros = block[i-1].numZero - l - r;
int diff = block[i].elem - block[i-1].elem;
diff -= 1;
if( zeros >= diff*2 && zeros <= diff*5 )
{
toLeft[i][l] = r;
}
}
}
}
// calculate toRight[i], which is only related to toLeft[i]
bool terminal = false;
for(int r = 0; r < 5; ++r)
{
if( r > block[i].numZero ) continue;
for(int l = 0; l < 5 && toRight[i][r] < 0; ++l)
{
if( toLeft[i][l] < 0 ) continue;
int num = r + l + block[i].numElem;
if( num >= 2 && num <= 5 )
{
toRight[i][r] = l;
terminal = true;
}
}
}
if( !terminal )
return false;
}
int size = int(block.size());
int pos = 0;
while( toRight[size-1][pos] < 0 && pos < 5 ) ++pos;
int remain = block.back().numZero - pos;
ans.clear();
if( remain == 1 )
{
++pos;
if( toRight[size-1][pos] < 0 )
return false;
ans.push_back( block.back().elem );
}
else if( remain > 1 )
{
int val = block.back().elem + 1;
if( remain&1 )
{
ans.push_back( val );
--remain;
}
for(int i = 1; i <= remain; ++i)
{
ans.push_back( val );
if( !(i&1) )
++val;
}
reverse(ans.begin(), ans.end());
}
int lj, rj = pos;
for(int i = size - 1; i > 0; --i)
{
lj = toRight[i][rj];
int num = lj + rj + block[i].numElem;
for(int k = 0; k < num; ++k)
ans.push_back( block[i].elem );
rj = toLeft[i][lj]; // cal the position, to which (i-1)th block extends;
int zeros = block[i-1].numZero - rj - lj;
int numVal = block[i].elem - block[i-1].elem - 1;
if( !numVal ) continue;
vector<int> temp;
int m = zeros/numVal, re = zeros%numVal;
int val = block[i-1].elem + 1;
for(int k = 1; k <= numVal; ++k)
{
for(int j = 0; j < m; ++j)
temp.push_back( val );
if( k <= re )
temp.push_back( val );
++val;
}
reverse(temp.begin(), temp.end());
ans.insert(ans.end(), temp.begin(), temp.end());
}
reverse(ans.begin(), ans.end());
cout << ans.back() << endl;
for(int i = 0; i < int(ans.size()); ++i)
cout << ans[i] << " ";
cout << endl;
return true;
}
int main()
{
ios::sync_with_stdio(false);
//freopen("data.tst", "r", stdin);
while( cin >> n )
{
note.resize( n );
for(int i = 0; i < n; ++i)
cin >> note[i];
if( !Work() )
cout << -1 << endl;
}
return 0;
}
解法二:构造解
把Vasya做n天的SummerReading想象成骑车穿过n米的长路。那么Vasya最快的方式是每隔两米,速度就增加1,最慢的方式是每隔五米,速度才增加1。输入中的非零数代表Vasya在此点时的速度。如果这个非零值小于Vasya以最慢的方式达到此点的速度,或者大于Vasya以最快的方式达到此点的速度,那么肯定是不合法的。
示例:0 0 2 2 3 3 0 0 4 4 0
最快方式:1 1 2 2 3 3 4 4 4 4 5
最慢方式:1 1 2 2 3 3 4 4 4 4 4
根据题中要求对于出现的书号,至少要出现两次。所以,对于达到的速度值至少要出现两次。最快的方式里面5只出现一次,为了满足条件,必须减1。最快方式就变成了 1 1 2 2 3 3 4 4 4 4 4。然后,利用最快方式的速度序列,从后往前把给定的原速度序列进行恢复即可。
#include <iostream>
#include <vector>
#include <map>
#include <cstdio>
#include <cstdlib>
using namespace std;
int n;
vector<int> note;
vector<pair<int,int> > maxNote;
vector<pair<int,int> > minNote;
bool Work()
{
maxNote.resize( n );
minNote.resize( n );
maxNote[0] = make_pair(1, 1);
minNote[0] = make_pair(1, 1);
if( note[0] > 1 )
return false;
for(int i = 1; i < n; ++i)
{
if( maxNote[i-1].second < 2 )
maxNote[i] = make_pair(maxNote[i-1].first, maxNote[i-1].second + 1);
else maxNote[i] = make_pair(maxNote[i-1].first+1, 1);
if( minNote[i-1].second < 5 )
minNote[i] = make_pair(minNote[i-1].first, minNote[i-1].second + 1);
else minNote[i] = make_pair(minNote[i-1].first+1, 1);
if( note[i] )
{
if( note[i] > maxNote[i].first || note[i] < minNote[i].first )
return false;
if( note[i] < maxNote[i].first )
maxNote[i] = make_pair(note[i], 5);
if( note[i] > minNote[i].first )
minNote[i] = make_pair(note[i], 1);
}
}
if( maxNote[n-1].second == 1 )
maxNote[n-1] = maxNote[n-2];
if( note[n-1] > maxNote[n-1].first )
return false;
return true;
}
int main()
{
//freopen("data.tst", "r", stdin);
while( cin >> n )
{
note.resize( n );
for(int i = 0; i < n; ++i)
cin >> note[i];
bool ans = Work();
if( !ans )
{
cout << -1 << endl;
continue;
}
note[n-1] = maxNote[n-1].first;
int len = 1;
for(int i = n - 2; i >= 0; --i)
{
if( maxNote[i].first >= note[i+1] )
{
note[i] = note[i+1];
++len;
if( len > 5 )
{
--note[i];
len = 1;
}
}
else
{
note[i] = maxNote[i].first;
len = 1;
}
}
cout << note[n-1] << endl;
for(int i = 0; i < n; ++i)
cout << note[i] << " ";
cout << endl;
}
return 0;
}
总结:算法题总是一题多解的,以前总是认为对于DP算法题,只要找出最优子结构,然后列出递推公式就ok了,但是从这道题来看,难点还止于此,后处理工作仍然不容小觑。关于构造解,个人的观点是,思路虽然很简单,但是要想在短时间内说服别人却是不容易的。