poj 3016 K-Monotonic 左偏树 + 贪心 + dp

//poj 3016 K-Monotonic
//分析:与2005年集训队论文黄源河提到的题目类似,给定序列a,求一序列b,b不减,且sigma(abs(ai-bi))最小。
//思路:去除左偏树(大根堆)一半的节点(向上取整),让左偏树的根节点上存放中位数;每个左偏树的根节点表示一个等值区间
//在本题中,我们将一段区间 与 一颗左偏树等同;将求调整给定数列 vi 为不减序列的代价 与 求取数列 bi 等同

  1 #include"iostream"
  2 #include"cstdio"
  3 #include"cstring"
  4 using namespace std;
  5 const int maxn = 1010;
  6 int v[maxn],l[maxn],r[maxn],d[maxn];    //左偏树节点信息,Merge维护
  7 int f[maxn][12],c[maxn][maxn];  //dp,c为cost
  8 int N,K;    //输入
  9 
 10 int tot,root[maxn],sum_del[maxn],num_del[maxn],sum_now[maxn],num_now[maxn]; //左偏树根节点信息,solve维护
 11 
 12 int Merge(int x,int y)
 13 {
 14     if(!x)
 15         return y;   //分治终点
 16     if(!y)
 17         return x;   //分治终点
 18     if(v[x]<v[y])
 19         swap(x,y);
 20     r[x] = Merge(r[x],y);   //分治
 21     if(d[l[x]]<d[r[x]])
 22         swap(l[x],r[x]);    //回溯维护l,r
 23     d[x] = d[r[x]]+1;   //回溯维护d
 24     return x;
 25 }
 26 
 27 int dis_sum(int tot)    //求一段区间(左偏树)上各项差绝对值的和
 28 {//从树上去除的节点都是v值大于或者等于中位数的,未去除的节点都是小于或者等于中位数的
 29     return sum_del[tot]-num_del[tot]*v[root[tot]]+num_now[tot]*v[root[tot]]-sum_now[tot];
 30 }
 31 
 32 void solve()    
 33 //solve能将给定的一个整数序列 v1 , v2 , … , vn,求一个不下降序列 b1≤b2≤…≤bn,使得数列 {vi} 和 {bi} 的各项之差的绝对值之和 |v1 - b1| + |v2 - b2| + … + |vn - bn| 最小。
 34 {
 35     //巧妙之处在于:数列 {bi} 中的元素是从数列 {vi} 中选取的;选取方法是: 当连续的 vi 不断的小于bi时,bi只能不断的取 这个连续区间 [vl...vr] 的中位数(用反证法容易证明这个结论),当 vi 上升到前一个下降区间的中位数以上的时候, bi 上升到与 vi 相同的值,这样我们就能通过贪心的方法得到一个最小各项差的绝对值之和
 36     int i,j,ans;
 37     for(i = 1; i<=N; ++i) {
 38         tot = ans = 0;
 39         for(j = i; j<=N; ++j) {
 40             root[++tot] = j;    
 41             //合并区间(左偏树)的一般化情况:考虑到当不断调整最后一个区间(左偏树)的中位数的时候可能会出现小于前一个区间(左偏树)中位数的情况,这时候就需要合并两个都不小的区间(左偏树),所以我们不妨将插入情况一般化为合并情况,对于每个元素都处理成一个区间(左偏树),用区间(左偏树)的合并操作来代替插入操作
 42             num_now[tot] = 1;   
 43             //由于我们将元素一般化为了一个区间,所以我们现在从区间的角度去重新理解这个做法:不断的加入一个新的区间,当新区间的中位数小于前一个区间的中位数时,就从后往前将最后一个区间和前一个区间做一趟合并操作,直到最后一个区间的中位数大于前一个区间;所求的 bi 就是各个区间的中位数(即左偏树的根节点对应的值)
 44             sum_now[tot] = v[j];    
 45             //聪明的读者可能会提问:这样能不能保证合并后区间的中位数没有在合并之前就被去除掉?答案是肯定的,一种比较直观的理解是:作平面直角坐标系,从后一个区间中位数的点向上平移,一定是先遇到前一个区间的中位数的点的水平高度,之后继续向上平移才有可能遇到最后一个区间内的其他点,画个图就能很容易理解合并后区间的中位数对应的点一定不会高于倒数第二个区间的中位数对应的点的水平高度的
 46             num_del[tot] = sum_del[tot] = 0;
 47             l[j] = r[j] = d[j] = 0;
 48             while(tot>1&&v[root[tot-1]]>v[root[tot]]) {
 49                 //如果靠后的区间(左偏树)的中位数小于前一个区间(左偏树),则合并这两个左偏树
 50                 ans -= dis_sum(tot-1)+dis_sum(tot); 
 51                 //各项差绝对值的和 回溯
 52                 root[tot-1] = Merge(root[tot-1],root[tot]);
 53                 num_now[tot-1] += num_now[tot];
 54                 sum_now[tot-1] += sum_now[tot];
 55                 num_del[tot-1] += num_del[tot];
 56                 sum_del[tot-1] += sum_del[tot];
 57                 --tot;
 58                 while(num_now[tot]>num_del[tot]+1) {    
 59                     //保证合并后区间的中位数对应的节点位于合并后的左偏树的根位置
 60                     --num_now[tot];
 61                     sum_now[tot] -= v[root[tot]];
 62                     ++num_del[tot];
 63                     sum_del[tot] += v[root[tot]];
 64                     root[tot] = Merge(l[root[tot]],r[root[tot]]);
 65                 }
 66                 ans += dis_sum(tot);
 67                 //各项差绝对值的和 加上 合并操作完成之后最后一个区间的 各项差绝对值的和
 68             }
 69             c[i][j] = min(c[i][j],ans);
 70         }
 71     }
 72 }
 73 
 74 int main()
 75 {
 76     int i,j,k;
 77     while(scanf("%d%d",&N,&K)&&(N||K)) {
 78         memset(f,0x3f,sizeof(f));
 79         memset(c,0x3f,sizeof(c));
 80         for(i = 1; i<=N; ++i) {
 81             scanf("%d",&v[i]);
 82             v[i] -= i;  
 83             //v[i] 为递增序列(的代价)相当于 v[i]-i 为不下降序列(的代价)
 84         }
 85         solve();
 86         for(i = 1; i<=N; ++i) {
 87             v[i] = -(v[i]+(i<<1));  
 88             //v[i] 为递减序列(的代价)相当于 -(v[i]+i) 为不下降序列(的代价)
 89         }
 90         solve();
 91         f[0][0] = 0;
 92         for(i = 1; i<=N; ++i) {
 93             for(j = 1; j<=K; ++j) {
 94                 for(k = j-1; k<i; ++k) {    
 95                     //f[前缀区间][前缀区间上的单调数列的个数],c[区间左端点][区间右端点]:将一段区间维护成单调区间的最小代价
 96                     f[i][j] = min(f[i][j],f[k][j-1]+c[k+1][i]);
 97                 }
 98             }
 99         }
100         printf("%d
",f[N][K]);
101     }
102     return 0;
103 }

//拓展:在本题目的基础上要求k个单调序列按照升序和降序交错排列

原文地址:https://www.cnblogs.com/AC-Phoenix/p/4266141.html