这周讲的东西主要是一些 DP 优化。
1800 文明与法治
题意:你知道未来 天内某只股票的走势,第 天股票买入价为每股 ,卖出价为每股 ,同时规定:
- 第 天至多购买 股,至多卖出 股;
- 某一天的买入或卖出均算作一次交易在。在两次交易之间至少要间隔 天;
- 任何时候手中的股票数不能超过 。
在第 天之前假设你有无限数目的钱,但是没有任何股票。请问 天后你最多能赚到多少钱?
解析:原题为[SCOI2010]股票交易。
一道经典的单调队列 DP。假设 为第 天有 只股票时赚得的最多的钱,那么 即是答案,因为最后一天前把股票卖完肯定更优。
那么考虑如何转移。如果第 天不进行交易,则
否则,可分成买入或卖出两种交易。若在第 天买入股票,则
即从 只股票买入到 只股票。同理,若卖出股票,则
即从 只股票卖出到 只股票。
现在考虑初始状态如何设置。由于第一天前有无限的钱,因此这 天内的任何一天都可以作为交易的第一天,买入至多 只股票。即
同时将其他状态都赋成极小值。
如果暴力转移以上方程,复杂度为 ,无法接受。观察转移方程,容易发现要求的是关于 的一段区间内的某个最大值,这个区间随着 向前移动。即经典的滑动窗口问题,可以用单调队列维护来优化 DP,于是复杂度就降到了 ,可以通过这道题。
代码:
//
// Created by 屋顶上的小丑 on 2023/3/13.
//
#include<cstdio>
#include<cstring>
const int maxn=2e3;
int T,MaxP,W,q[maxn+5];
int ap[maxn+5],bp[maxn+5];
int as[maxn+5],bs[maxn+5];
long long f[maxn+5][maxn+5];
int max(int a,int b)
{
return (a>b)?a:b;
}
int main()
{
scanf("%d%d%d",&T,&MaxP,&W);
for(int i=1;i<=T;i++)
scanf("%d%d%d%d",&ap[i],&bp[i],&as[i],&bs[i]);
memset(f,-0x3f,sizeof(f));
for(int i=1;i<=T;i++)
for(int j=0;j<=as[i];j++)
f[i][j]=-j*ap[i];
for(int i=1;i<=T;i++)
{
for(int j=0;j<=MaxP;j++)
f[i][j]=max(f[i][j],f[i-1][j]);
if(i<=W)
continue;
int las=i-W-1;
int l=1,r=0;
for(int j=0;j<=MaxP;j++)
{
while(l<=r&&q[l]<j-as[i]) l++;
if(l<=r)
f[i][j]=max(f[i][j],f[las][q[l]]+(q[l]-j)*ap[i]);
while(l<=r&&f[las][q[r]]+q[r]*ap[i]<=f[las][j]+j*ap[i]) r--;
q[++r]=j;
}
l=1,r=0;
for(int j=MaxP;j>=0;j--)
{
while(l<=r&&q[l]>j+bs[i]) l++;
if(l<=r)
f[i][j]=max(f[i][j],f[las][q[l]]+(q[l]-j)*bp[i]);
while(l<=r&&f[las][q[r]]+q[r]*bp[i]<=f[las][j]+j*bp[i]) r--;
q[++r]=j;
}
}
printf("%lld",f[T][0]);
return 0;
}
1511 wennitao卖冰墩墩
题意:一条路上有 个工厂,第 个工厂距离第 个工厂 ,有 个冰墩墩,在这里搭建售卖点的费用为 ,保证 。现在希望把所有的冰墩墩运往售卖点售卖,只能从编号小的工厂运往编号大的工厂,一个冰墩墩运送一个单位距离费用为 ,请问在哪写地方搭建售卖点能够使总费用最小?
解析:非常经典的斜率优化。
设 表示最后一个售卖点在第 个工厂的最小费用,可以得到转移方程:
设 前缀和为 , 前缀和为 ,则
这里已经可以看出斜率优化的雏形了,我们继续转化,令 ,则上式为
设 ,即 ,到这里已经完全可以看出这就是裸的斜率优化了。由于要求 值,而斜率 单调递增,横坐标 也是单调递增的,因此直接用单调队列维护一个下凸包即可。复杂度为 。
代码:
//
// Created by 屋顶上的小丑 on 2023/3/13.
//
#include<cstdio>
#include<cstring>
const int maxn=1e6;
int n,x[maxn+5],p[maxn+5];
int q[maxn+5],c[maxn+5];
long long sum1[maxn+5],sum2[maxn+5];
long long f[maxn+5];
long long a(int i)
{
return c[i]+x[i]*sum1[i]-sum2[i];
}
long long b(int i)
{
return f[i]+sum2[i];
}
double k(int i)
{
return (double)x[i];
}
long long X(int i)
{
return sum1[i];
}
long long Y(int i)
{
return b(i);
}
double slope(int i,int j)
{
return (double)(Y(i)-Y(j))/(X(i)-X(j));
}
long long min(long long x,long long y)
{
return (x<y)?x:y;
}
int main()
{
scanf("%d",&n);
f[0]=0;
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&x[i],&p[i],&c[i]);
sum1[i]=sum1[i-1]+p[i];
sum2[i]=sum2[i-1]+1ll*p[i]*x[i];
}
int l=1,r=1;
for(int i=1;i<=n;i++)
{
while(l<r&&slope(q[l],q[l+1])<k(i)) l++;
f[i]=b(q[l])+a(i)-1ll*sum1[q[l]]*x[i];
while(l<r&&slope(q[r-1],q[r])>slope(q[r],i)) r--;
q[++r]=i;
}
printf("%lld",f[n]);
return 0;
}
1802 破败的灯塔国(加强版)
题意:一条直线上有 座连续的山峰,第 座山峰高度为 。若在第 座山峰建立一座高度为 的灯塔,它能照亮第 座山峰当且仅当 。现在请问在每座山峰上建立一座灯塔,想要让它照亮所有山峰的最小高度是多少。
解析:原题为[JSOI2016]灯塔。
设第 座山峰建立灯塔的最小高度为 ,那么显然
我们只考虑 的情况,因为对于 的情况我们只用反过来再做一遍即可。
然后就能发现决策单调性了。由于 ,因此当决策点 时,满足 ,即 ,满足四边形不等式。然后就很好处理了,用二分栈或者分治都能实现,复杂度为 。代码是分治写法。
代码:
//
// Created by 屋顶上的小丑 on 2023/3/13.
//
#include<cstdio>
#include<cmath>
const int maxn=1e6;
int n,h[maxn+5];
double ans[maxn+5];
inline double max(double a,double b)
{
return a>b?a:b;
}
inline double calc(int i,int j)
{
return h[j]+sqrt(abs(i-j));
}
template<class T>
void swap(T &x,T &y)
{
T temp=x;
x=y;
y=temp;
}
void solve(int l,int r,int L,int R)
{
if(l>r)
return ;
int mid=(l+r)>>1,pos=0;
double las=0;
for(int i=L;i<=mid&&i<=R;i++)
{
double now=calc(mid,i);
if(las<now)
{
las=now;
pos=i;
}
}
ans[mid]=max(ans[mid],las);
solve(l,mid-1,L,pos);
solve(mid+1,r,pos,R);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&h[i]);
solve(1,n,1,n);
for(int i=1;i<=n/2;i++)
{
swap(h[i],h[n-i+1]);
swap(ans[i],ans[n-i+1]);
}
solve(1,n,1,n);
for(int i=n;i>=1;i--)
printf("%d\n",(int)ceil(ans[i]-h[i]));
return 0;
}
1812 zhongzero的黑白树
题意:给定一个 个点 条边无向带权连通图,每条边是黑色或白色,让你求恰好有 条白色边的最小权生成树,输出该生成树的边权和即可。
解析:原题为[国家集训队]Tree I。
挺简单的题,用到了类似 wqs 二分(因为感觉并不算严格的 wqs 二分)的思路。
要求最小权生成树,显然要用最小生成树算法。我们思考一下 kruskal 算法求解最小生成树的原理,它需要先按边权从小到大进行排序再依次对边进行选择,边权越小的边会优先被选择。因此我们可以通过改变白边的权值来改变它们被选中的机会。我们考虑二分一个额外值 ,将白边的权值全部加上 再跑 kruskal,如果最后跑出的结果中白边数量小于 ,说明我们的 太大了,把它调小;反之则可以将 调大。由于数据一定有解,如此一定能够得到一个恰好 条白边的最小权生成树,此时将得到的边权和减去 即是答案。
//
// Created by 屋顶上的小丑 on 2023/3/13.
// 第四道题,用了algorithm库中的sort
//
#include<cstdio>
#include<cmath>
#include<algorithm>
#define INF 1e9
using namespace std;
const int maxn=1e5;
struct edge
{
int x;
int y;
int z;
int col;
}s[maxn+5];
bool cmp(edge a,edge b)
{
if(a.z==b.z)
return a.col<b.col;
return a.z<b.z;
}
int n,m,need,maxx,k;
int fa[maxn+5],ans,sum;
int getfa(int x)
{
if(fa[x]==x)
return x;
return fa[x]=getfa(fa[x]);
}
bool check(int x)
{
for(int i=1;i<=m;i++)
if(!s[i].col)
s[i].z+=x;
for(int i=1;i<=n+1;i++)
fa[i]=i;
sum=k=0;
sort(s+1,s+m+1,cmp);
int tot=0;
for(int i=1;i<=m;i++)
{
if(tot==n-1)
break;
int a=getfa(s[i].x);
int b=getfa(s[i].y);
if(a==b)
continue;
else
{
tot++;
fa[a]=b;
sum+=s[i].z;
k+=(s[i].col^1);
}
}
return k>=need;
}
void clean(int x)
{
for(int i=1;i<=m;i++)
if(!s[i].col)
s[i].z-=x;
}
int main()
{
scanf("%d%d%d",&n,&m,&need);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d%d",&s[i].x,&s[i].y,&s[i].z,&s[i].col);
s[i].x++;
s[i].y++;
maxx=max(maxx,s[i].z);
}
int l=-INF,r=INF,mid;
while(l<=r)
{
mid=(l+r)>>1;
if(check(mid))
{
ans=sum-need*mid;
l=mid+1;
}
else
r=mid-1;
clean(mid);
}
printf("%d\n",ans);
return 0;
}