原题下载:http://icpc.baylor.edu/download/worldfinals/problems/icpc2013.pdf
题目翻译:
问题描述
俄罗斯套娃是一些从外到里大小递减的传统的俄罗斯木头玩偶组成的。当你打开一个俄罗斯套娃时,里面就会露出一个同样的俄罗斯套娃,再打开,就会再露出一个,不断重复。
俄罗斯的俄罗斯套娃博物馆最近收藏了一些外形相似的俄罗斯套娃集,只是里面嵌套的玩偶数量不相等。不幸的是,有一群过分热情的(和明显无人监督的)孩子们拆了他们,并放在一行上。有n个玩偶在一上,每个都有一个整数的大小,你需要重新组装套娃集,你既不知道套娃集的数量,也不知道某个套娃集内玩偶的数量,你只知道一个完好的套娃集内的玩偶大小是从1到某个数字m
在组装套娃集时,你必须遵守下列规则:
1.你只能将一个玩偶或者套娃集放入一个更大的玩偶中
2.你只能把相邻两个俄罗斯套娃组合在一起
3.已经被合并的玩偶是不能再重新拆出来的。
你的时间很宝贵,你只想尽快的组装好。唯一需要耗时的部分为打开一个玩偶并马上关上它。所以你要尽可能少的做这种操作。比如说:合并[1,2,6]与[4],你需要将大小为4和6的两个玩偶拆开。合并[1,2,5]与[3,4]代价为3。
求将n个玩偶重新拼成一些完好的俄罗斯套娃的最小代价。
输入格式
第一行一个数n,第二行包含n个数,依次表示每个玩偶的大小。
输出 格式
如果答案存在,输出一个数表示将n个玩偶重新拼成一些完好的俄罗斯套娃的最小代价。否则输出“Impossible”
样例输入
7
1 2 1 2 4 3 3
样例输出
Impossible
样例输入
7
1 2 3 2 4 1 3
样例输出
7
数据规模和约定
1<=n<=500 , 1<=玩偶大小<=500
题目大意:
有n个套娃排成一列,我们要将它们合并成若干个完整的套娃(即这个套娃的最大一层为m,它里面包含从1~n-1所有大小的套娃各一个),每次只能选择相邻两个合并,且已经合并的套娃不可以拆开(在与其它套娃合并时可以临时拆开),问完成合并至少需要将套娃拆开多少次(例如将(3, 2)与(1)合并需要拆开两次)
思路分析:
这是一道区间DP的题目,看到数据范围和时间限制,大概是一个(O(n^3))左右的复杂度。我们先预处理出(g(i,\,j)),表示将序列上的第i个套娃到第j个套娃合并成一个套娃(不需要是完整的套娃)需要拆开的最小次数。显然当i~j中存在相同大小的套娃时,(g(i,\,j)=infty)(他们不可能被合并成一个)
我们可以轻松写出转移方程[g(i,\,j)=max_{(ile k<j)}{left{g(i,\,k)+g(k+1,\,j)+f(i,\,k,\,j) ight}},]其中这个(f(i,\,k,\,j))表示将合并完的(1~k)和(k + 1~j)合并起来所需的最小代价。通过观察可以发现我们不需要把两边的所有套娃全都拆开,但是至少要将一边的套娃全部拆开,而另一边只要拆到剩下的那一组套娃正巧可以放到完全拆开的那一组的最小的那一个套娃里面就好了,这等价于将求出两组套娃中的分别的最小值的较大者,小于这个较大者的套娃全部不用拆开而剩余的(包括这个较大者都需要拆开)。至此我们已经得到了(f(i,\,k,\,j))的计算方法,然后(g(i,\,j))就已经搞定了
接下来我们定义(p(k))表示将前k个合并成若干个(多少个?不需要知道)完整的套娃需要的最小拆开次数,然后得到转移方程[p(k)=min_{1le i<k}{{p(i)+g(i+1,\,k)}},]然后就解决了。
算法流程:
1.DP计算(g(i,\,j)),在计算过程中对i~j进行局部排序,之后枚举k,并同时分别维护1~k与k+1~j中的最小值,然后计算得到解
在这里我有一点要说明的,排序的时候我建议使用计数排序,有两个原因:首先,套娃大小的范围与n的范围是相同的,使用计数排序可以保证渐进复杂度为线性,而快排之类的就会退化成(O(nlog n))。第二,更重要的是,计数排序更容易维护每一个值的名次(否则需要二分查找,又是一个log)。
2.DP计算(p(k))
3.(p(n))即为所求
参考代码:
1 //date 20140123 2 #include <cstdio> 3 #include <cstring> 4 5 const int maxn = 505; 6 const int INF = 9999999; 7 8 inline int getint() 9 { 10 int ans(0); char w = getchar(); 11 while(w < '0' || w > '9')w = getchar(); 12 while('0' <= w && w <= '9') 13 { 14 ans = ans * 10 + w - '0'; 15 w = getchar(); 16 } 17 return ans; 18 } 19 20 inline int max(int a, int b){return a > b ? a : b;} 21 inline int min(int a, int b){return a < b ? a : b;} 22 23 int n; 24 int a[maxn]; 25 int count[maxn], count2[maxn]; 26 int sum[maxn]; 27 int exist[maxn]; 28 int g[maxn][maxn]; 29 int f[maxn]; 30 31 inline int check(int l, int r) 32 { 33 memset(exist, 0, sizeof exist); 34 for(int i = l; i <= r; ++i)if(exist[a[i]])return false; 35 else ++exist[a[i]]; 36 for(int i = 1; i <= r - l + 1; ++i)if(!exist[i])return false; 37 return true; 38 } 39 40 int main() 41 { 42 freopen("matryoshka.in", "r", stdin); 43 freopen("matryoshka.out", "w", stdout); 44 45 n = getint(); 46 for(int i = 1; i <= n; ++i) a[i] = getint(); 47 memset(g, 0x7F, sizeof g); 48 for(int i = n; i; --i) 49 { 50 g[i][i] = 0; 51 for(int j = i + 1; j <= n; ++j) 52 { 53 g[i][j] = INF; 54 int Max(0), flag(0), Min1(INF), Min2(INF), ans(INF), Min(INF); 55 memset(count2, 0, sizeof count2); 56 memset(count, 0, sizeof count); 57 58 for(int w = i; w <= j; ++w) 59 if(count2[a[w]]){flag = 1; break;} 60 else {++count2[a[w]]; ++count[a[w]]; Min2 = min(Min2, a[w]); Max = max(Max, a[w]);} 61 if(flag)continue; 62 sum[Min2 - 1] = 0; 63 64 for(int w = Min2; w <= Max; ++w)sum[w] = sum[w - 1] + count[w]; 65 66 for(int k = i; k < j; ++k) 67 { 68 --count2[a[k]]; 69 Min1 = min(Min1, a[k]); 70 if(Min2 == a[k])for(Min2; !count2[Min2]; ++Min2); 71 72 ans = min(ans, g[i][k] + g[k + 1][j] + (j - i + 1) - sum[max(Min1, Min2) - 1]); 73 } 74 g[i][j] = ans; 75 } 76 } 77 78 f[0] = 0; 79 for(int i = 1; i <= n; ++i)f[i] = INF; 80 if(a[1] == 1)f[1] = 0; 81 for(int i = 2; i <= n; ++i) 82 { 83 for(int j = 0; j < i; ++j) 84 if(check(j + 1, i)){f[i] = min(f[i], f[j] + g[j + 1][i]);} 85 } 86 87 88 if(f[n] != INF) printf("%d ", f[n]); 89 else printf("Impossible "); 90 return 0; 91 }
需要注意的事项:
1.在计算(g(i,\,j))时,如果需要赋值成无穷大,不要直接使用INT_MAX,因为我们会出现一个相加比较,这样一加就导致无穷大变成了负数
2.一定要注意递推的顺序
3.计算(g(i,\,j))的时候不建议使用递归,因为我们需要维护一个当前i~j的顺序,需要另开数组,如果开在全局变量则需要开很多,如果开在局部变量则容易爆栈