第一题(入门题)
319C - Kalila and Dimna in the Logging Industry
题意:有 n n n棵树,每棵树有一个高度 a i a_i ai,每次操作可以使一棵树高度减1,每次操作完需要充电,充电只能在树的高度为0的地方(这个位置的树被砍完了),花费为 b i b_i bi,保证 a a a数组升序且 a 1 = 1 a_1=1 a1=1, b b b数组降序且 b n = 0 b_n=0 bn=0,求把所有树都砍完的最小花费。
思路:考虑到
b
n
=
0
b_n=0
bn=0,那么我们肯定是前面选几棵树砍完,然后砍第
n
n
n棵树,再利用第
n
n
n个位置一直充电,免费地把剩下所有的树砍完。所以考虑
d
p
i
dp_i
dpi表示砍倒第
i
i
i棵树的最小花费,最后输出
d
p
n
dp_n
dpn即可。显然,转移如下:
d
p
i
=
m
i
n
(
d
p
j
+
a
i
∗
b
j
)
(
j
<
i
)
dp_i=min(dp_j+a_i*b_j)\;\;(j<i)
dpi=min(dpj+ai∗bj)(j<i)
显然是一个斜率优化的形式,进行转化(省略取min):
d
p
i
=
b
j
∗
a
i
+
d
p
j
dp_i=b_j*a_i+dp_j
dpi=bj∗ai+dpj
{
y
=
d
p
i
k
=
b
j
x
=
a
i
b
=
d
p
j
\left \{ \begin{array}{c} y=dp_i \\ k=b_j\;\;\\ x=a_i\;\;\\ b=dp_j \end{array} \right.
⎩
⎨
⎧y=dpik=bjx=aib=dpj
化简成
y
=
k
x
+
b
y=kx+b
y=kx+b的形式,发现是一个最纯朴的形式 😃 ,直接CHT
struct CHT {
struct line {
ll k, b;
line() {}
line(ll k, ll b): k(k), b(b) {}
double intersect(line l) {
double db = l.b - b;
double dk = k - l.k;
return db / dk;
}
ll operator () (ll x) {
return k * x + b;
}
};
vector<double> x;
vector<line> li;
void init(line l) {
x.push_back(-LINF);
li.push_back(l);
}
void addLine(line l) {
while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
x.pop_back();
li.pop_back();
}
if (!li.empty()) {
x.push_back(l.intersect(li.back()));
}
li.push_back(l);
}
ll query(ll qx) {
int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
--id;
return li[id](qx);
}
};
ll dp[maxn],a[maxn],b[maxn];
void solve(){
int n;
cin>>n;
rep(i,1,n) cin>>a[i];
rep(i,1,n) cin>>b[i];
mem(dp,0x3f);
dp[1]=0;
CHT cht;
cht.init(CHT::line(b[1],dp[1]));
rep(i,2,n){
dp[i]=cht.query(a[i]);
cht.addLine(CHT::line(b[i],dp[i]));
}
cout<<dp[n]<<endl;
}
第二题
题意: n n n个物品,每个物品有一个价值 C i C_i Ci,把这n个物品分成若干段,每段 ( l , r ) (l,r) (l,r)的价值为 ( ∑ i = l r C i + j − i + 1 + L ) 2 (\sum_{i=l}^rC_i+j-i+1+L)^2 (∑i=lrCi+j−i+1+L)2,求分割后最小的价值总和。
dp方程:
d
p
i
=
m
i
n
(
d
p
j
+
(
p
r
e
i
−
p
r
e
j
+
i
−
j
−
L
)
2
)
(
j
<
i
)
dp_i=min(dp_j+(pre_i-pre_j+i-j-L)^2)\;\;(j<i)
dpi=min(dpj+(prei−prej+i−j−L)2)(j<i)
令
A
=
p
r
e
i
+
i
,
B
=
p
r
e
j
+
j
+
L
A
只与
i
有关,
B
只与
j
有关
A=pre_i+i\;\;,\;B=pre_j+j+L \;\;\;\;\;A只与i有关,B只与j有关
A=prei+i,B=prej+j+LA只与i有关,B只与j有关
那么我们的
d
p
方程就可以化为
(
省略了取
m
i
n
)
:
那么我们的dp方程就可以化为(省略了取min):
那么我们的dp方程就可以化为(省略了取min):
d
p
i
=
d
p
j
+
(
A
−
B
)
2
=
d
p
j
+
A
2
+
B
2
−
2
A
B
\begin{aligned}dp_i&=dp_j+(A-B)^2\\ &=dp_j+A^2+B^2-2AB \end{aligned}
dpi=dpj+(A−B)2=dpj+A2+B2−2AB
再化成斜率优化的形式:
d
p
i
−
A
2
=
−
2
A
B
+
d
p
j
+
B
2
dp_i-A^2=-2AB+dp_j+B^2
dpi−A2=−2AB+dpj+B2
{
y
=
d
p
i
−
A
2
k
=
−
2
B
x
=
A
b
=
d
p
j
+
B
2
\left \{ \begin{array}{c} y=dp_i-A^2 \\ k=-2B\;\;\;\;\;\;\\ x=A \;\;\;\;\;\;\;\;\;\;\;\\ b=dp_j+B^2 \end{array} \right.
⎩
⎨
⎧y=dpi−A2k=−2Bx=Ab=dpj+B2
原式子已经化成了y=kx+b的形式,接下来只需要利用CHT实现即可。
struct CHT {
struct line {
ll k, b;
line() {}
line(ll k, ll b): k(k), b(b) {}
double intersect(line l) {
double db = l.b - b;
double dk = k - l.k;
return db / dk;
}
ll operator () (ll x) {
return k * x + b;
}
};
vector<double> x;
vector<line> li;
void init(line l) {
x.push_back(-1e30);
li.push_back(l);
}
void addLine(line l) {
while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
x.pop_back();
li.pop_back();
}
if (!li.empty()) {
x.push_back(l.intersect(li.back()));
}
li.push_back(l);
}
ll query(ll qx) {
int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
--id;
return li[id](qx);
}
};
int n;
ll L,a[maxn],pre[maxn],dp[maxn];
void solve(){
cin>>n>>L;
L++;
rep(i,1,n) cin>>a[i],pre[i]=pre[i-1]+a[i];
CHT cht;
dp[0]=0;
cht.init(CHT::line(-2*(a[0]+L),dp[0]+(pre[0]+L)*(pre[0]+L)));
rep(i,1,n){
dp[i]=cht.query(pre[i]+i)+(pre[i]+i)*(pre[i]+i);
cht.addLine(CHT::line( -2*(pre[i]+i+L), dp[i]+ (pre[i]+i+L) * (pre[i]+i+L) ) );
}
cout<<(long long)dp[n]<<endl;
}
第三题
1083E - The Fair Nut and Rectangles
题意:给定 n ( n < = 1 e 6 ) n(n<=1e6) n(n<=1e6)个矩形 每个矩形的左下角位置 ( 0 , 0 ) (0,0) (0,0),右上角 ( x i , y i ) (x_i,y_i) (xi,yi),并且有一个花费 c i c_i ci,所有矩形没有嵌套,选一些矩形,使得他们面积的交 减去 花费的和最大。
思路:首先,矩形没有嵌套,那么当我们将所有矩形右上角按
x
x
x小到大排序后,
y
y
y一定是大到小排序的。那么假如当前要选第
i
i
i个矩形,上一个选的矩形是第
j
j
j个,那么我们的总价值改变为,如图:
+
(
x
i
−
x
j
)
∗
y
i
−
a
i
+(x_i-x_j)*y_i-a_i
+(xi−xj)∗yi−ai
所以我们就能得到一个dp式:
d
p
i
=
m
a
x
(
d
p
j
+
(
x
i
−
x
j
)
∗
y
i
−
a
i
)
(
j
<
i
)
dp_i=max(dp_j+(x_i-x_j)*y_i-a_i) (j<i)
dpi=max(dpj+(xi−xj)∗yi−ai)(j<i)
看到这样形式的式子,很容易想到斜率优化,于是化简式子至一下形式:
d
p
i
−
x
i
∗
y
i
+
a
i
=
−
x
j
∗
y
i
+
d
p
j
dp_i-x_i*y_i+a_i=-x_j*y_i+dp_j
dpi−xi∗yi+ai=−xj∗yi+dpj
{
y
=
d
p
i
−
x
i
∗
y
i
+
a
i
k
=
−
x
j
x
=
y
i
b
=
d
p
j
\left \{ \begin{array}{c} y&=dp_i-x_i*y_i+a_i \\ k&=-x_j\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\ \\ x&=y_i \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; \\ b&=dp_j\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\ \end{array} \right.
⎩
⎨
⎧ykxb=dpi−xi∗yi+ai=−xj =yi=dpj
原式子已经化成了
y
=
k
x
+
b
y=kx+b
y=kx+b的形式,接下来只需要利用CHT实现即可。
struct CHT {
struct line {
ll k, b;
line() {}
line(ll k, ll b): k(k), b(b) {}
double intersect(line l) {
double db = l.b - b;
double dk = k - l.k;
return db / dk;
}
ll operator () (ll x) {
return k * x + b;
}
};
vector<double> x;
vector<line> li;
void init(line l) {
x.push_back(-LINF);
li.push_back(l);
}
void addLine(line l) {
while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
x.pop_back();
li.pop_back();
}
if (!li.empty()) {
x.push_back(l.intersect(li.back()));
}
li.push_back(l);
}
ll query(ll qx) {
int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
--id;
return li[id](qx);
}
};
struct rectangle{
ll x,y,a;
bool operator < (const rectangle& ts) const{
return x<ts.x;
}
}s[1000005];
int n;
ll dp[1000005];
void solve(){
cin>>n;
rep(i,1,n) cin>>s[i].x>>s[i].y>>s[i].a;
sort(s+1,s+1+n);
CHT cht;
cht.init(CHT::line(0,0));
ll mx=0;
rep(i,1,n){
dp[i]=-cht.query(-s[i].y)-s[i].a+s[i].x*s[i].y;
cht.addLine(CHT::line(-s[i].x,-dp[i]));
mx=max(mx,dp[i]);
}
cout<<mx<<endl;
}
第四题
题意:给定一个数组 a a a,可以进行最多一次操作:将一个数插到另一个位置,相当于选一个区间循环移位一下,求最大的 ∑ i = 1 n a i ∗ i \sum_{i=1}^na_i*i ∑i=1nai∗i
假设循环移位的区间为
(
i
,
j
)
(i,j)
(i,j),发现
i
i
i左边的价值和
j
j
j右边的价值并没有改变,只有中间这部分有改变,因此我们只计算最大的改变量,具体需要分两种情况讨论:
(1)右移:
(2)左移:
tips:本题比较特殊,因为等式左边原本就没有含
i
,
j
i,j
i,j的项,所以化简后的式子中带有
i
和
j
i和j
i和j的项,可以任取一个作为
k
k
k,另一个作为
x
x
x,但是由于一个是单调的,另一个不是单调的,所以我们要取单调的那个作为我们的
k
k
k,不然就得cdq分治了= =
最后两个公式分别做两次CHT即可 😃
struct CHT {
struct line {
ll k, b;
line() {}
line(ll k, ll b): k(k), b(b) {}
double intersect(line l) {
double db = l.b - b;
double dk = k - l.k;
return db / dk;
}
ll operator () (ll x) {
return k * x + b;
}
};
vector<double> x;
vector<line> li;
void init(line l) {
x.clear();
li.clear();
x.push_back(-LINF);
li.push_back(l);
}
void addLine(line l) {
while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
x.pop_back();
li.pop_back();
}
if (!li.empty()) {
x.push_back(l.intersect(li.back()));
}
li.push_back(l);
}
ll query(ll qx) {
int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
--id;
return li[id](qx);
}
};
int n;
ll a[maxn],dp,pre[maxn],tot,mx;
struct LINE{
ll k,b;
bool operator < (const LINE& ts)const{
return k>ts.k;
}
}li[maxn];
void solve(){
cin>>n;
rep(i,1,n) cin>>a[i],tot+=i*a[i],pre[i]=pre[i-1]+a[i];
CHT cht;
cht.init(CHT::line(n,pre[n]));
per(i,n-1,1){
dp=-cht.query(-a[i])+pre[i]-i*a[i];
mx=max(mx,dp);
cht.addLine(CHT::line(i,pre[i]));
}
cht.init(CHT::line(-1,pre[0]));
rep(i,2,n){
dp=-cht.query(a[i])+pre[i-1]-i*a[i];
mx=max(mx,dp);
cht.addLine(CHT::line(-i,pre[i-1]));
}
cout<<mx+tot<<endl;
}
第五题
1388E - Uncle Bogdan and Projections
题意:给 n ( n < = 2000 ) n\;(n<=2000) n(n<=2000)条平行于 x x x轴的线段,且都位于第一象限,没有重合的线段。选一个方向,使所有线段按这个方向投影到 x x x轴上,使所有端点中,最右边的点 − \;-\; −最左边的点 最小,求这个最小值。
思路:首先方向可以随便取,有无限种可能,但是我们可以发现,我们选的这个方向必定经过两条线段的两个端点(一条的左端点和另一条的右端点 或 一条的右端点和另一条的左端点),也就是说投影后的线段必定至少有两条是相连的,否则,我们可以将这个方向调整使距离更小。基于这个事实,所有可能的方向就只有
n
2
n^2
n2个,如果对于这
n
2
n^2
n2个方向去计算距离,复杂度是
o
(
n
3
)
o(n^3)
o(n3)的。
因此我们再考虑如何快速计算
x
x
x最大和最小的位置,假设当前方向与
x
x
x负半轴的角度为
a
l
p
h
a
alpha
alpha:
如图所示,投影后的
x
x
x坐标可以表示成斜率的形式,因此我们只需要求
m
a
x
和
m
i
n
max和min
max和min然后
m
a
x
−
m
i
n
max-min
max−min即可,即维护两个CHT,一个求
m
i
n
min
min,一个求
m
a
x
max
max。
不过最后代码中还有很多小细节,比如:
1.求方向部分,先枚举两条线段,然后可以求出两个方向,然而在这中间的方向是不能取的(因为投影之后会有重合),因此相当于求区间交,最后拿出区间的两个端点才是有用的方向。
2.CHT部分,求
m
i
n
min
min的和求
m
a
x
max
max的CHT逻辑是不一样的,不能同时插入线段,并且本题的线段可能有相同的斜率,当有相同的斜率时,应该手动保留
b
b
b最小的直线。
struct CHT {
struct line {
ll k, b;
line() {}
line(ll k, ll b): k(k), b(b) {}
double intersect(line l) {
double db = l.b - b;
double dk = k - l.k;
return db / dk;
}
double operator () (double x) {
return k * x + b;
}
};
vector<double> x;
vector<line> li;
void init(line l) {
x.clear();
li.clear();
x.push_back(-LINF);
li.push_back(l);
}
void addLine(line l) {
while (li.size() >= 2 && l.intersect(li[li.size() - 2]) <= x.back()) {
x.pop_back();
li.pop_back();
}
if (!li.empty()) {
x.push_back(l.intersect(li.back()));
}
li.push_back(l);
}
double query(double qx) {
int id = upper_bound(x.begin(), x.end(), qx) - x.begin();
--id;
return li[id](qx);
}
};
struct Vector{
ll x,y;
int f;
double at;
bool operator < (const Vector& ts) const{ //极角排序
if(at==ts.at) return f<ts.f;
return at<ts.at;
// if(atan2(y,x)==atan2(ts.y,ts.x)) return f<ts.f;
// return atan2(y,x)<atan2(ts.y,ts.x);
}
};
vector <Vector> a;
struct node{
ll l,r,y;
bool operator < (const node& ts) const{
if(y==ts.y) return r<ts.r;
return y>ts.y;
}
}li[maxn];
int cmp1(node A,node B){
if(A.y==B.y) return A.l<B.l;
return A.y>B.y;
}
int cmp2(node A,node B){
if(A.y==B.y) return A.r>B.r;
return A.y<B.y;
}
int n;
ll l[maxn],r[maxn],y[maxn];
void solve(){
cin>>n;
rep(i,1,n) cin>>l[i]>>r[i]>>y[i],li[i].l=l[i],li[i].r=r[i],li[i].y=y[i];
rep(i,1,n){
rep(j,i+1,n){
if(y[i]==y[j]) continue;
Vector t;
t.x=l[i]-r[j];
t.y=y[i]-y[j];
t.f=1;
if(t.y>0) t.x*=-1,t.y*=-1,t.f*=-1;
t.at=atan2(t.y,t.x);
a.pb(t);
t.x=r[i]-l[j];
t.y=y[i]-y[j];
t.f=-1;
if(t.y>0) t.x*=-1,t.y*=-1,t.f*=-1;
t.at=atan2(t.y,t.x);
a.pb(t);
}
}
if((int)a.size()==0) {
ll mn=LINF,mx=-LINF;
rep(i,1,n){
mn=min(mn,l[i]);
mx=max(mx,r[i]);
}
cout<<fixed<<setprecision(10)<<1.0*(mx-mn)<<endl;
return ;
}
CHT chtmx,chtmn;
sort(li+1,li+1+n,cmp1);
chtmn.init(CHT::line(li[1].y,li[1].l));
rep(i,2,n){
if(li[i].y!=li[i-1].y){
chtmn.addLine(CHT::line(li[i].y,li[i].l));
}
}
sort(li+1,li+1+n,cmp2);
chtmx.init(CHT::line(-li[1].y,-li[1].r));
rep(i,2,n){
if(li[i].y!=li[i-1].y){
chtmx.addLine(CHT::line(-li[i].y,-li[i].r));
}
}
sort(a.begin(),a.end());
int now=0;
double ans=1e30;
double mn=1e30,mx=-1e30;
for(auto V:a){
if(now==0 && V.f==1){
mn= chtmn.query(-1.0*V.x/V.y);
mx=-chtmx.query(-1.0*V.x/V.y);
ans=min(ans,mx-mn);
}
else if(now==1 && V.f==-1){
mn= chtmn.query(-1.0*V.x/V.y);
mx=-chtmx.query(-1.0*V.x/V.y);
ans=min(ans,mx-mn);
}
now+=V.f;
}
cout<<fixed<<setprecision(10)<<ans<<endl;
}
第六题
题意:给你一棵以1号点为根的树,一开始有一个人在1号结点,当他在 i i i号结点时,他可以跳到 i i i的子树中的任意一个结点 j j j,花费为 a i ∗ b j a_i*b_j ai∗bj,问最后跳到任意一个叶子结点的最小花费。
令
d
p
i
dp_i
dpi表示
i
i
i到叶子结点的最小花费,转移为:
d
p
i
=
m
i
n
(
d
p
j
+
a
i
∗
b
j
)
(
j
∈
s
o
n
i
)
dp_i=min(dp_j+a_i*b_j)\;\;\;\;(j\in son_i)
dpi=min(dpj+ai∗bj)(j∈soni)
bool Q;
struct Line {
mutable ll k, m, p;
bool operator<(const Line& o) const {
return Q ? p < o.p : k < o.k;
}
};
struct CHT : multiset<Line> {
const ll inf = LINF;
ll div(ll a, ll b){
return a / b - ((a ^ b) < 0 && a % b);
}
bool isect(iterator x, iterator y) {
if (y == end()) { x->p = inf; return false; }
if (x->k == y->k) x->p = x->m > y->m ? inf : -inf;
else x->p = div(y->m - x->m, x->k - y->k);
return x->p >= y->p;
}
void add(ll k, ll m) {
auto z = insert({k, m, 0}), y = z++, x = y;
while (isect(y, z)) z = erase(z);
if (x != begin() && isect(--x, y)) isect(x, y = erase(y));
while ((y = x) != begin() && (--x)->p >= y->p)
isect(x, erase(y));
}
ll query(ll x) {
assert(!empty());
Q = 1; auto l = *lower_bound({0,0,x}); Q = 0;
return l.k * x + l.m;
}
};
int n;
ll a[maxn],b[maxn],dp[maxn],sz[maxn];
vector <int> G[maxn];
void dfs1(int u,int fa){
sz[u]=1;
for(auto v:G[u]) if(v!=fa){
dfs1(v,u);
sz[u]+=sz[v];
}
}
void dfs(int u,int fa,CHT& now){
int leaf=1;
int mx=0,id;
for(auto v:G[u]) if(v!=fa){
leaf=0;
if(sz[v]>mx){
id=v;
mx=sz[v];
}
}
if(leaf){
dp[u]=0;
now.add(-b[u],-dp[u]);
return ;
}
dfs(id,u,now);
for(auto v:G[u]) if(v!=fa && v!=id){
CHT tmp;
dfs(v,u,tmp);
for(auto i:tmp){
now.add(i.k,i.m);
}
}
dp[u]=-now.query(a[u]);
now.add(-b[u],-dp[u]);
}
void solve(){
cin>>n;
rep(i,1,n) cin>>a[i];
rep(i,1,n) cin>>b[i];
rep(i,1,n-1){
int u,v;
cin>>u>>v;
G[u].pb(v);
G[v].pb(u);
}
dfs1(1,0);
CHT cht;
dfs(1,1,cht);
rep(i,1,n) cout<<dp[i]<<" ";
}