再看斜率优化打牌

emmmm,马上要比赛了,最近正在复习打牌

又看了遍斜率优化dp,感觉对他的理解上了一个新的高度呐

斜率优化dp可以优化掉一个n


一般来说,如果要求一个状态时要遍历前面已经求过的所有状态的话,就可以考虑使用斜率优化

再求第k个状态时对于前面的所有状态如果k2比k1更优就有

Y(k2)-Y(k1)X(k2)-X(k1)<f(k)


我们就可以使用斜率优化

这里我们要保证x(k2)-x(k1)>0所以一般需要排序,或者x(k)是sum之类的且f(k)也是递增的

(说实话感觉如果把上面的<换成>,然后f(k)是递减的也能使用斜率优化,这个等下问问我的牌友)


斜率优化是我们有一个队列,这个队列的斜率是递增排列的

然后在计算dp[k]的时候,我们从队列头开始找,如果第二个比第一个更优,我们就把head出队

知道找到第二个没有第一个优秀

那么我们可知这时候第一个是最优的(斜率是递增的嘛)

而且被出队的点对后面也没有影响,因为斜率是递增的,也就是


那么dp[k]就可以通过这个状态递推出来

然后我们下一个要求第k+1个状态嘛,我们就要保证能递推出k+1这个状态的状态必须进过队列一次(出不出队就是另外一回事了)

然后比如说我要把状态为j的入队,这个时候我就要保证我序列斜率的单调性就行了,这里可能会导致一些节点出队,可以分类讨论论证这些出队的节点绝对不会是最优的


说了这么多,还是看一个题目最为直接

HDU3045

题意大概就是我有n头牛,每头牛有一个快乐值,他们要出去玩,我要把这群牛分成几组,每组的人数不能少于m

然后对于每组的牛,他们的快乐值会减少为这一组中快乐值最少的牛的快乐值

然后问如何可以让所有牛快乐值的减少量最小


还有一个超级重要的东西(幸亏今天又做了一道dp题

就是我们在初始化的时候,不能简单的memset为0

有的时候最初的情况是不能从0推出来的,这时候就需要我们手动初始化一下

#include <cstdio>
#include <cstring>
#include <cmath>
#include <iostream>
#include <algorithm>
#define maxn 400100
#define ll long long
using namespace std;


ll a[maxn];
ll dp[maxn];
ll que[maxn],sum[maxn];

ll getup(ll k2,ll k1)//求yk2-yk1
{
    return (dp[k2]-sum[k2]+a[k2+1]*k2)-(dp[k1]-sum[k1]+a[k1+1]*k1);
}

ll getdown(ll k2,ll k1)//求xk2-xk1
{
    return (a[k2+1]-a[k1+1]);
}

void getdp(int i,int j)
{
     dp[i]=dp[j]+(sum[i]-sum[j])-a[j+1]*(i-j);//状态转移方程,从j退出i
}

int main()
{
    ll n,m;
    while(~scanf("%lld %lld",&n,&m))
    {
        for (ll k=1;k<=n;k++)
            scanf("%lld",&a[k]);
        sort(a+1,a+n+1);

        sum[0]=0;a[0]=0;
        for (int k=1;k<=n;k++)
            sum[k]=sum[k-1]+a[k];

        ll head(0),tail(1);
        que[head]=0;dp[0]=0;

        for (ll k=1;k<=n;k++)
        {
            while(head+1<tail&&getup(que[head+1],que[head])<=k*(getdown(que[head+1],que[head])))//找到最优的那个点
                head++;

            getdp(k,que[head]);//状态转移

            ll j=k-m+1;
            if (j<m)//看对下一个状态,我必须把什么入队
                continue;

            while(head+1<tail&&getup(j,que[tail-1])*getdown(que[tail-1],que[tail-2])<=getup(que[tail-1],que[tail-2])*getdown(j,que[tail-1]))
                tail--;
            que[tail]=j;tail++;//入队操作
        }
        printf("%lld\n",dp[n]);
    }
    return 0;
}


对于初始化的题目,这里有一道hdu3669


这道题的排序也很值得写博客,开始自己把方程推出来之后还感觉要用线段树求区间最值

排序之后,真的是很神奇

这题的思路就不写了,网上的博客一搜一大堆


下面放上代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>
#include <algorithm>
#define maxn 50500
#define ll long long

using namespace std;

struct Data
{
    ll x,y;
};

bool cmp(Data aa,Data bb)
{
    if (aa.x==bb.x)
        return aa.y<bb.y;
    else
        return aa.x>bb.x;
}

Data a[maxn],b[maxn];
ll que[maxn],head,tail;

ll dp[maxn][110];

ll getup(ll j,ll k2,ll k1)
{
    return dp[k2][j-1]-dp[k1][j-1];
}

ll getdown(ll j,ll k2,ll k1)
{
    return b[k1+1].x-b[k2+1].x;
}

void getdp(ll i,ll j,ll k)
{
    dp[i][j]=dp[k][j-1]+b[k+1].x*b[i].y;
    return;
}

int main()
{
    ll n,m;
    while(~scanf("%lld %lld",&n,&m))
    {
        for (ll k=1;k<=n;k++)
            scanf("%lld %lld",&a[k].x,&a[k].y);

        sort(a+1,a+n+1,cmp);

        ll nb(0),Max(0);
        for (ll k=1;k<=n;k++)
            if (Max<a[k].y)
            {
                nb++;b[nb]=a[k];
                Max=a[k].y;
            }
            //x从大到小,y从小到大
        memset(dp,0,sizeof(dp));

        for (ll k=1;k<=nb;k++)
            dp[k][1]=b[1].x*b[k].y;//初始化

        for (ll j=2;j<=m;j++)
        {
            head=0;tail=1;
            que[head]=1;dp[1][j]=b[1].x*b[1].y;//初始化
            for (ll i=2;i<=nb;i++)
            {
                while(head+1<tail&&getup(j,que[head+1],que[head])<=getdown(j,que[head+1],que[head])*b[i].y)
                    head++;

                getdp(i,j,que[head]);

                while(head+1<tail&&getup(j,i,que[tail-1])*getdown(j,que[tail-1],que[tail-2])<=getup(j,que[tail-1],que[tail-2])*getdown(j,i,que[tail-1]))
                    tail--;
                que[tail]=i;tail++;
            }
        }

        ll Min(10000001000000);
        for (ll j=1;j<=m;j++)
            Min=min(Min,dp[nb][j]);

        printf("%lld\n",Min);
    }
    return 0;
}
(感觉还是自己的代码长得漂亮哈 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值