石子合并问题(内含BZOJ3229 AC代码)


我们来看这样一道题目

有n堆石子排成一列,每堆石子有一个重量w[i],
每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,能够使得总合并代价达到最小。
n<=100,w<=100


显然这是一道普及水平的区间DP
fi..j 表示将[i,j]合并成一堆所需要的最小代价
ai
sumi=ij=1ai
fi..j=fi..k+fk+1..j+sumjsumi1
正确做法是 O(n3)

#include<cstdio>
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int w[101],n,f[102][102];
main()
{
    int x;
    scanf("%d",&n); 
    for (int i=1;i<=n;i++)
        scanf("%d",w+i),
        w[i]+=w[i-1];
    memset(f,63,sizeof(f));
    for (int i=1;i<=n;i++) f[i][i]=0;
    for (int i=n-1;i>=1;i--)
        for (int j=i+1;j<=n;j++)
            for (int k=i;k<=j-1;k++)
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+w[j]-w[i-1]);
    printf("%d",f[1][n]);
}


我们注意到,这是石子堆排列成一条线的
那如果这些石子堆是排成一个圆呢?
我们可以这么想
每两个相邻的石子堆之间连一条线段,那样每个石子堆都往外发出两条线段,每次合并(i,j)实际上是砍掉i
到j的线段,原先i到i的前一个石子堆的线段变成了j到i的前一个石子堆的线段
最初是n条线段,合并n-1次,最后肯定有一个线段用不到
直线排列时这条用不到的线段是已知的
但圆排列时是未知的
那我们就尝试枚举每一条所谓的线段
把这些石子堆当成直线排列,然后后面再加上一条排列相同的石子堆!
最终答案就是 minfi..i+n1 i=1..n
复杂度仍是 O(n3)

#include<cstdio>
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int w[201],n,f[202][202];
main()
{
    int x;
    scanf("%d",&n); 
    for (int i=1;i<=n;i++)
        scanf("%d",w+i),
        w[i+n]=w[i];
    for (int i=1;i<=n<<1;++i) w[i]+=w[i-1];
    memset(f,63,sizeof(f));
    for (int i=1;i<=n<<1;i++) f[i][i]=0;
    for (int i=n<<1;i>=1;i--)
        for (int j=i+1;j<=n<<1;j++)
            for (int k=i;k<=j-1;k++)
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+w[j]-w[i-1]);
    f[0][0]=1<<30;
    for(int i=1;i<=n;++i)
        f[0][0]=min(f[0][0],f[i][i+n-1]);
    printf("%d\n",f[0][0]);
}


求最大值类似,不再赘述
那我们变一下n的范围
n<=3000
那我们需要 O(n2) 做法
怎么办?
考虑四边形不等式优化

大神比我讲的更清楚

    1、区间包含的单调性:如果对于 i≤i’ <j≤j’,有 w(i’,j)≤w(i,j’),那么说明w具有区间包含的单调性。(可以形象理解为如果小区间包含于大区间中,那么小区间的w值不超过大区间的w值)

    2、四边形不等式:如果对于 i≤i’<j≤j’,有 w(i,j)+w(i’,j’)≤w(i’,j)+w(i,j’),我们称函数w满足四边形不等式。(可以形象理解为两个交错区间的w的和不超过小区间与大区间的w的和)

  下面给出两个定理:

    1、如果上述的 w 函数同时满足区间包含单调性和四边形不等式性质,那么函数 m 也满足四边形不等式性质

      我们再定义 s(i,j) 表示 m(i,j) 取得最优值时对应的下标(即 i≤k≤j 时,k 处的 w 值最大,则 s(i,j)=k)。此时有如下定理

    2、假如 m(i,j) 满足四边形不等式,那么 s(i,j) 单调,即 s(i,j)≤s(i,j+1)≤s(i+1,j+1)。


总之我们就可以优化成 O(n2) 的了

#include<cstdio>
#include<cstring>
#include<iostream> 
using namespace std;
int n,w[3005];
int f[3005][3005],s[3005][3005];
main()
{
    scanf("%d",&n);
    memset(f,63,sizeof(f));
    for (int i=1;i<=n;++i)
        scanf("%d",w+i),
        f[i][i]=0,s[i][i]=i,
        w[i]+=w[i-1];
    for (int i=n;i>=1;--i)
        for (int j=i+1;j<=n;++j)
            for (int k=s[i][j-1];k<=s[i+1][j];++k)
                if (f[i][j]>f[i][k]+f[k+1][j]+w[j]-w[i-1])
                    f[i][j]=f[i][k]+f[k+1][j]+w[j]-w[i-1],
                    s[i][j]=k;
    printf("%d\n",f[1][n]);
}


环形类似
那求最大值呢?
这好像不太符合四边形不等式的优化
但仍然可以 O(n2)
以cogs上石子归并加强版的环形为例

#include<cstdio>
#include<cstring>
#include<iostream> 
using namespace std;
int n,w[4005];
int f[4005][4005],s[4005][4005];
main()
{
    freopen("stone3.in","r",stdin);
    freopen("stone3.out","w",stdout);
    scanf("%d",&n);
    for (int i=1;i<=n;++i)
        scanf("%d",w+i),
        s[i+n][i+n]=i+n,s[i][i]=i,w[n+i]=w[i];
    for (int i=1;i<=n*2;++i) w[i]+=w[i-1];
    for (int i=n*2-1;i>=1;--i)
        for (int j=i+1;j<=min(n*2-1,i+n-1);++j)
            f[i][j]=max(f[i][i]+f[i+1][j],f[j][j]+f[i][j-1])+w[j]-w[i-1];
    for(int i=1;i<=n;++i) f[0][0]=max(f[0][0],f[i][i+n-1]);
    printf("%d\n",f[0][0]); 
}


注意这是严格 O(n2)
那我们来个终极版
n<=50000,求最小值
怎么办?
考虑GarsiaWachs算法
ACDreamer

#include<cstdio>
#define M 50001
using namespace std;
int n,a[M],tot,ans;
int in()
{
    int t=0;char ch=getchar();
    while (ch>'9'||ch<'0') ch=getchar();
    while (ch>='0'&&ch<='9') t=(t<<1)+(t<<3)+ch-48,ch=getchar();
    return t;
}
void unions(int x)
{
    int tmp=a[x]+a[x-1];
    ans+=tmp;
    for (int i=x;i<tot;++i) a[i]=a[i+1];
    int j;
    for (j=x-1;a[j-1]<tmp&&j>1;--j) a[j]=a[j-1];
    a[j]=tmp;--tot;
    for (int d=tot-j;j>2&&a[j-2]<=a[j];d=tot-j)
        unions(j-1),
        j=tot-d;
}
main()
{
    n=in();
    for (int i=1;i<=n;++i) a[i]=in();
    for (int i=1;i<=n;++i)
    {
        a[++tot]=a[i];
        while (tot>2&&a[tot-2]<=a[tot]) unions(tot-1);
    }
    for (;tot>1;) unions(tot);
    printf("%d\n",ans);
}


据说这个是 O(n2) 的,但是在一般数据下它可以到达 O(nlogn)
这本来是应该平衡树做的
写得很匆忙而且水平很渣
找个时间再来仔细研究……

以下是一份使用Treap实现的C++代码,仅供参考。 ```c++ #include <bits/stdc++.h> using namespace std; const int INF = 0x3f3f3f3f; const int MAXN = 1e5 + 5; int n, m; int root, cnt; struct Node { int val, key; int size; int ch[2]; } tr[MAXN]; inline int new_node(int val) { tr[++cnt].val = val; tr[cnt].key = rand(); tr[cnt].size = 1; return cnt; } inline void pushup(int p) { tr[p].size = 1 + tr[tr[p].ch[0]].size + tr[tr[p].ch[1]].size; } inline void split(int p, int k, int &x, int &y) { if (!p) { x = y = 0; return; } if (tr[p].val <= k) { x = p; split(tr[p].ch[1], k, tr[p].ch[1], y); } else { y = p; split(tr[p].ch[0], k, x, tr[p].ch[0]); } pushup(p); } inline int merge(int x, int y) { if (!x || !y) return x + y; if (tr[x].key < tr[y].key) { tr[x].ch[1] = merge(tr[x].ch[1], y); pushup(x); return x; } else { tr[y].ch[0] = merge(x, tr[y].ch[0]); pushup(y); return y; } } inline void insert(int val) { int x, y; split(root, val, x, y); root = merge(merge(x, new_node(val)), y); } inline void remove(int val) { int x, y, z; split(root, val, x, z); split(x, val - 1, x, y); y = merge(tr[y].ch[0], tr[y].ch[1]); root = merge(merge(x, y), z); } inline int kth(int k) { int p = root; while (p) { if (tr[tr[p].ch[0]].size >= k) { p = tr[p].ch[0]; } else if (tr[tr[p].ch[0]].size + 1 == k) { return tr[p].val; } else { k -= tr[tr[p].ch[0]].size + 1; p = tr[p].ch[1]; } } return -1; } inline int query_min() { int p = root; int res = INF; while (p) { res = min(res, tr[p].val); p = tr[p].ch[0]; } return res; } int main() { srand(time(NULL)); scanf("%d%d", &n, &m); for (int i = 1, opt, x; i <= n; ++i) { scanf("%d%d", &opt, &x); if (opt == 1) { insert(x); } else { remove(tr[cnt].val); } } printf("%d\n", query_min()); while (m--) { int k; scanf("%d", &k); printf("%d\n", kth(k)); } return 0; } ``` 代码实现中使用了一个结构体 `Node` 来表示平衡树的每一个节点。其中 `val` 表示节点存储的值,`key` 表示随机生成的优先级,`size` 表示以该节点为根的子树大小,`ch[0]` 和 `ch[1]` 分别表示左右儿子的下标。接下来就是一些经典的平衡树操作,例如 `split` 和 `merge` 用于分裂和合并平衡树,`insert` 和 `remove` 用于插入和删除节点,`kth` 和 `query_min` 用于查询平衡树中第 $k$ 小的数和最小值。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值