比赛的时候我第一时间就认为贪心是不对的,于是就写了个dfs型的DP,配了个状态树,就当是练习DP了。
这两天看网上大家说贪心最近的那个也可以,但是我一直还没理解,求证明。
言归正传,本题一般DP的话状态(len,arr[0..p])异常的大,有1001个长度,1000个位置,每个位置可能存在1000个不同的值,都保存必然超内存。
另外考虑到每次的操作都是针对序列末端的少了元素进行的,序列前面大段的内容都没有变化,于是考虑使用状态树压缩存储。
由于题目要求只要存在一个解就可以返回了,所以每个状态需要记录的内容非常简单,就是从此下去搜索是否会失败(若到此状态可以成功则算法就直接退出了,不需要继续)。算法进行时若发现当前状态会导致失败,则直接返回上一个状态。
每个节点有一个布尔标志表示从根到此节点为止的序列是否一定会导致最终结果失败,对于还不确定的自然是标记为否啦;以及一个子节点列表。这里我为了省事,就用了STL的map(比赛的时候我为了验证每个元素是否都出现了偶数次,还做了编号重排,把内元素的编号变到0到999,如果做了这一步,在节点里面也可以开一个1000的数组)。
注意:我们发现某个时刻arr[0..k]对应的状态会失败并不意味着arr[0..j],0<=j<k对应的状态也会失败,这也就是为什么每个节点都有一个failed标记而不用是否存在某个节点作为判断依据的原因。
代码:
#include <iostream>
#include <map>
using namespace std;
int arr[1005];
inline void move_down(const int down, const int up) {
for(int i=down + 1; i < up; i++){
arr[i - 1]=arr[i];
}
}
inline void move_up(const int down, const int up) {
for(int i=up - 1; i > down; i--){
arr[i]=arr[i - 1];
}
}
struct node {
bool failed;
map<int, node> children;
node():failed(false){}
node(const bool f):failed(f){}
};
//若在状态(p,arr[0..p])失败,则在失败状态树中记录该点
node root(false);
bool check_failed(const int p) {
node *curr=&root;
for(int i=0; i <= p; i++){
map<int, node>::iterator it=curr->children.find(arr[i]);
if(it != curr->children.end()){
curr=&it->second;
}else
return false;
}
return curr->failed;
}
void set_failed(const int p) {
node *curr=&root;
for(int i=0; i <= p; i++){
curr=&(curr->children[arr[i]]);
}
curr->failed=true;
}
bool dfs(const int p) {//check arr[0..p] inclusive
if(p == -1)
return true;
if(check_failed(p))
return false;
int key=arr[p];
for(int i=1; i < 6 && p - i >= 0; i++){
if(key == arr[p - i]){
move_down(p - i, p);
if(dfs(p - 2))
return true;
move_up(p - i, p);
arr[p - i]=arr[p]=key;
}
}
set_failed(p);
return false;
}
int main() {
int n;
while(cin >> n){
for(int i=0; i < n; i++){
cin >> arr[i];
} //input
if(n % 2 == 1)
cout << 0 << endl;
else{
//root.children.clear();//对于相同的状态,结论也一定相同,而且使用map作为子节点表时间效率影响不大,故不需要每次清空状态树。
cout << (dfs(n - 1) ? 1 : 0) << endl;
}
}
return 0;
}