三分:

([AHOI2014/JSOI2014]宅男计划)

题目描述

外卖店一共有N种食物,分别有1到N编号。第i种食物有固定的价钱Pi和保质期Si。第i种食物会在Si天后过期。JYY是不会吃过期食物的。

比如JYY如果今天点了一份保质期为1天的食物,那么JYY必须在今天或者明天把这个食物吃掉,否则这个食物就再也不能吃了。保质期可以为0天,这样这份食物就必须在购买当天吃掉。

JYY现在有M块钱,每一次叫外卖需要额外付给送外卖小哥外送费F元。

送外卖的小哥身强力壮,可以瞬间给JYY带来任意多份食物。JYY想知道,在满足每天都能吃到至少一顿没过期的外卖的情况下,他可以最多宅多少天呢?

输入输出格式

输入格式:

第一行包含三个整数M,F和N。

接下来N行,第i行包含两个整数Pi和Si。

输出格式:

输出仅包含一行一个整数表示JYY可以宅的最多的天数。

输入输出样例

输入样例#1:

32 5 2
5 0
10 2

输出样例#1:

3

说明

【样例说明】

JYY的最佳策略是:

第一天买一份食物1和一份食物2并且吃一份食物1;

第二天吃一份食物2;

第三天买一份食物1并且吃掉。

【数据规模与约定】

对于100%的数据满足0<=Si<=10^18,1<=F,Pi,M<=10^18,1<=N<=200

题目地址:https://www.luogu.org/problemnew/show/P4040


个人思路:

  • 第一眼看题面感觉是DP,但看看数据范围只能想到Miller-Rabin,但是实际上三分也OK
  • 可以发现在钱数固定的情况下,单次购买食物的数量与维持时间是一个单峰函数
  • 二分+三分即可求解
  • T=(M-cost(k)*c-c*f)/(cost(k+1)-cost(k))+k*c ,k=D((M-C*f)/C)

#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
int n;int cnt;ll m;ll f;
struct food
{
    ll cst;ll kep;
    friend bool operator <(food a,food b){return a.kep<b.kep;}
}tp[210],fo[210];
inline ll cost(ll t)//计算存活到t的花费 
{
    ll res=0;
    for(int i=1;i<=cnt;i++)
    {
        if(res<0){return -1;}
        if(t>fo[i].kep){res+=fo[i].cst*fo[i].kep;t-=fo[i].kep;}
        else {res+=fo[i].cst*t;return res;}
    }return -1;//这里是避免爆INF 
}
inline ll calcd(ll pm)//计算花多少钱可以存活多少天 
{
    ll l=0;ll r=pm;
    while(l<r)//单调可以二分 
    {
        ll mid=(l+r+1)/2;ll y=cost(mid);
        if(y==-1||y>pm){r=mid-1;}else {l=mid;}
    }
    return r;
}
inline ll calct(ll r)//计算T 
{
    ll rest=m-r*f;if(rest<0||rest>m){return 0;}//还是避免爆longlong 
    ll pm=rest/r;ll k=calcd(pm);if(k==0){return 0;} 
    ll c2=cost(k+1);ll c1=cost(k);if(c2==-1){return r*k;}
    else {rest-=c1*r;return rest/(c2-c1)+r*k;}
}
int main()
{
    scanf("%lld%lld%d",&m,&f,&n);
    for(int i=1;i<=n;i++){scanf("%lld%lld",&tp[i].cst,&tp[i].kep);}
    sort(tp+1,tp+n+1);tp[0].kep=-1;fo[0].kep=-1;
    for(int i=1;i<=n;i++)//单调栈去掉无用的东西 
    {while(cnt!=0&&tp[i].cst<fo[cnt].cst){cnt--;}fo[++cnt]=tp[i];}
    for(int i=cnt;i>=1;i--){fo[i].kep-=fo[i-1].kep;}//后向差分方便计算 
    ll l=1;ll r=m/f+1;//三分法求函数最值 
    while(r-l>5)//缩小区间 
    {
        ll x1=l+(r-l)/3;ll x2=l+((r-l)*2)/3;
        ll y1=calct(x1);ll y2=calct(x2);
        if(y1<y2){l=x1;}else {r=x2;}
    }
    ll res=calct(l);//然后暴力枚举最大值 
    for(ll i=l+1;i<=r;i++){res=max(res,calct(i));}
    printf("%lld",res);return 0;//拜拜程序~ 
}

单调队列优化DP:

([SCOI2010]股票交易)

题目描述

最近 \text{lxhgww} 又迷上了投资股票,通过一段时间的观察和学习,他总结出了股票行情的一些规律。

通过一段时间的观察, \text{lxhgww} 预测到了未来 T 天内某只股票的走势,第 ii 天的股票买入价为每股 AP_i,第 i 天的股票卖出价为每股 BP_i​(数据保证对于每个 i,都有 AP_i \geq BP_i​),但是每天不能无限制地交易,于是股票交易所规定第 i 天的一次买入至多只能购买 AS_i股,一次卖出至多只能卖出 BS_i股。

另外,股票交易所还制定了两个规定。为了避免大家疯狂交易,股票交易所规定在两次交易(某一天的买入或者卖出均算是一次交易)之间,至少要间隔 W 天,也就是说如果在第 i 天发生了交易,那么从第 i+1天到第 i+W 天,均不能发生交易。同时,为了避免垄断,股票交易所还规定在任何时间,一个人的手里的股票数不能超过 \text{MaxP}

在第 1 天之前,\text{lxhgww}手里有一大笔钱(可以认为钱的数目无限),但是没有任何股票,当然,T 天以后,\text{lxhgww}想要赚到最多的钱,聪明的程序员们,你们能帮助他吗?

输入输出格式

输入格式:

输入数据第一行包括 3 个整数,分别是 T,\text{MaxP},W。

接下来 T 行,第 i 行代表第 i-1 天的股票走势,每行 4 个整数,分别表示 AP_i,\ BP_i,\ AS_i,\ BS_i

输出格式:

输出数据为一行,包括 1 个数字,表示 \text{lxhgww} 能赚到的最多的钱数。

输入输出样例

输入样例#1:

5 2 0
2 1 1 1
2 1 1 1
3 2 1 1
4 3 1 1
5 4 1 1

输出样例#1: 

3

说明

对于 100\%100% 的数据,0\leq W<T\leq 2000,1\leq\text{MaxP}\leq2000

对于所有的数据,1\leq BP_i\leq AP_i\leq 1000,1\leq AS_i,BS_i\leq\text{MaxP}

题目地址:https://www.luogu.org/problemnew/show/P2569


个人思路:

  • 第三种情况的转移方程f_{i,j}=max(f_{i,j}\ \,,\ \,f_{i-w-1,k}+k\times ap_i)-j\times ap_i(j - as_i \leqslant k < j)
    第四种情况的转移方程f_(i,j)​=max(f_{i,j}\ \,,\ \,f_{i-w-1,k}+k \times bp_i)-j \times bp_i (j < k \leqslant j + bs_i)
  • 可以发现f_{i-w-1,k}+k\times ap_i可由单调队列维护

#include <cstdio>
#include <cstring>
#define Min(x , y) ((x) < (y) ? (x) : (y))
#define Max(x , y) ((x) > (y) ? (x) : (y))

int n , m , ap , bp , as , bs , w , ans = 0 , f[2001][2001] , l , r , q[2001];
// f[i][j] 表示第 i 天后拥有 j 张股票赚的最多钱数
// l , r , q[x] 用于单调队列

int main(){
    scanf("%d%d%d" , &n , &m , &w);
    memset(f , 128 , sizeof(f)); // 128 实际上给 f 数组赋的是 -inf,可以试试看
    for(int i = 1 ; i <= n ; i++){
        scanf("%d%d%d%d" , &ap , &bp , &as , &bs);
        for(int j = 0 ; j <= as ; j++)
            f[i][j] = -1 * j * ap; // 第 1 种情况,直接赋
        for(int j = 0 ; j <= m ; j++)
            f[i][j] = Max(f[i][j] , f[i - 1][j]); // 第 2 种情况,转移
        if(i <= w)
            continue; // 因为第 3,4 种情况都有 i - w - 1,如果 i <= w,会出现负下标
        // 第 3 种情况,从小转移到大
        l = 1 , r = 0; // 单调队列准备
        for(int j = 0 ; j <= m ; j++){
            while(l <= r && q[l] < j - as)
                l++; // 把过期的元素扔掉
            while(l <= r && f[i - w - 1][q[r]] + q[r] * ap <= f[i - w - 1][j] + j * ap)
                r--; // 更新单调队列元素
            q[++r] = j; // 插入最新元素
            if(l <= r)
                f[i][j] = Max(f[i][j] , f[i - w - 1][q[l]] + q[l] * ap - j * ap); 
            // 如果单调队列里有元素,即可转移
        }
        // 第 4 种情况,从大转移到小
        l = 1 , r = 0; // 单调队列再次准备
        for(int j = m ; j >= 0 ; j--){
            while(l <= r && q[l] > j + bs)
                l++; // 把过期的元素扔掉
            while(l <= r && f[i - w - 1][q[r]] + q[r] * bp <= f[i - w - 1][j] + j * bp)
                r--; // 更新单调队列元素
            q[++r] = j; // 插入最新元素
            if(l <= r)
                f[i][j] = Max(f[i][j] , f[i - w - 1][q[l]] + q[l] * bp - j * bp); 
            // 如果单调队列里有元素,即可转移
        }
    }
    for(int i = 0 ; i <= m ; i++)
        ans = Max(ans , f[n][i]); // 最后,可以留股票(实际上不留任何股票的就是最优答案),找出最优答案
    printf("%d\n" , ans);
    return 0;
}

 总结:

  • 从题面上看的话,两题最大的区别是数据范围和是否卖出这一条件
  • 从数据范围上我们可以推断出第一题使用三分,第二题使用线性DP