DTOJ 2022.11.08 测试 题解
A 光
题目大意
有四个格子,左上、右上、左下、右下分别可以填一个值 \(a,b,c,d\),每个格子上的值 \(x\) 可以对自己贡献 \(x\) 的亮度,对相邻的两个格子贡献 $\lfloor \frac{x}{2} \rfloor $ 的亮度 ,对对角线的格子贡献 $\lfloor \frac{x}{4} \rfloor $ 的亮度 ,最后要满足每个格子的亮度分别大于等于 \(A,B,C,D\) (\(A,B,C,D\le 1500\))求 \(a+b+c+d\) 的最小值
题解
1. \(O(n^4)\)
直接枚举四个.(时间复杂度中的 \(n\) 表示 \(max\{A,B,C,D\}\) )
2. \(O(n^3)\)
只需要枚举三个,算出每个格子还需要多少,剩下的一个就可以 \(O(1)\) 算了.
3. \(O(n^2\log n)\)
二分答案,然后枚举对角线上的点,这时候可以算出剩下的两个格子还需要多少,算出大致的值可以上下枚举一下.
4. \(O(1)\)
(准确来说是 \(O(2^4\cdot 4^3\cdot 5^4)\) ,这么说其实跟上面那个差不多快)
考场上不知道为什么想到了这个神奇做法(感觉还是很可行的)
注意到限制是一系列的线性不等式,可以考虑线性规划()
(普通的线性规划其实就是二元一次不等式组,然后求 \(\max\{ax+by\}\) 这样的东西, 这个时候把不等式组看成一系列直线切出来的半平面的交,然后 \(\max\{ax+by\}\) 可以看成 \(ax+by=t\) 这条直线与可行区域有交点,\(t\) 最大的时候其实就一定是一个多边形的顶点)
改成四元不等式其实差不多,四个方程一定只有一个解,所以一定也只有一个顶点,这个时候一定取到最小值.
但是我们注意到 \(a,b,c,d\geq 0\) 这个条件,所以我们枚举哪些是零,总共有 \(2^4\) 种情况.
最后求出来之后也跟刚才一样上下枚举一下.
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1505;
const int D = 10;
double a[6][6],b[6],B[6];
int res[6];
int ans=N<<2;
void solve()
{
for(int i=1; i<=4; i++)
{
int mnp=1;
for(int j=1; j<=4; j++) if(a[j][i]>a[mnp][i]) mnp=j;
for(int j=1; j<=4; j++)
if(j!=mnp)
{
double d=a[j][i]/a[mnp][i];
for(int k=1; k<=4; k++) a[j][k]-=a[mnp][k]*d;
b[j]-=b[mnp]*d;
}
}
for(int i=1; i<=4; i++) res[i]=(int)b[i]/a[i][i];
for(int i=max(res[1]-D,0); i<=res[1]+D; i++) for(int j=max(res[2]-D,0); j<=res[2]+D; j++)
for(int k=max(res[3]-D,0); k<=res[3]+D; k++) for(int l=max(res[4]-D,0); l<=res[4]+D; l++)
if(i+j/2+k/2+l/4>=B[1] and j+i/2+l/2+k/4>=B[2] and l+j/2+k/2+i/4>=B[4] and k+i/2+l/2+j/4>=B[3])
if(i+j+k+l<ans) ans=i+j+k+l;
}
int main()
{
for(int i=1; i<=4; i++) scanf("%lf",&B[i]);
for(int t=0; t<16; t++)
{
for(int i=1; i<=4; i++) b[i]=B[i];
for(int i=1; i<=4; i++) for(int j=1; j<=4; j++) a[i][j]=0.5;
for(int i=1; i<=4; i++) a[i][i]=1,a[i][5-i]=0.25;
for(int j=1; j<=4; j++) if((t>>(j-1))&1)
{
for(int i=1; i<=4; i++) a[j][i]=0;
b[j]=0; a[j][j]=1;
}
solve();
}
printf("%d\n",ans);
return 0;
}
B 奶茶代金券
还没补题,看着像简单贪心,但细节实在太多了(((((((ww不会写贪心)
C 函数复合
题面
给定 \(n\) 个函数 \(F_1(x), F_2(x), \dots, F_n(x)\),每个函数为以下三种之一:
- \(F(x) = x + w\);
- \(F(x) = \min(x, w)\);
- \(F(x) = \max(x, w)\)。
给定 \(q\) 次操作,每次操作为以下四种之一:
- \(1 ~ p ~ w\):表示令 \(F_p(x) = x + w\);
- \(2 ~ p ~ w\):表示令 \(F_p(x) = \min(x, w)\);
- \(3 ~ p ~ w\):表示令 \(F_p(x) = \max(x, w)\);
- \(4 ~ x\):表示询问 \((F_n \circ F_{n - 1} \circ \dots \circ F_1)(x)\) 的值,其中 \(F \circ G\) 表示函数 \(F\) 与 \(G\) 的复合,即 \((F \circ G)(x) = F(G(x))\)。
请维护以上操作。
对于所有测试数据,保证 \(1 \le n \le 10 ^ 5\),\(1 \le q \le 3 \times 10 ^ 5\),\(op_i \in \{1, 2, 3\}\),\(1 \le p \le n\),\(1 \le x \le 10 ^ 8\),保证任意时刻第 \(1\) 种函数中的 \(w\) 在 \([1, 200]\) 之内,第 \(2,3\) 种函数中的 \(w\) 在 \([1, 10 ^ 8]\) 之内。
题解
这题我测试的时候写得很顺利,对拍都过了,不知道为什么交上去只有 75 分,把 assert 删掉了之后就过了(我直接问号?
呃呃所以怎么写呢
方法1
注意到无论多少次 \(\min,\max,+\) 函数复合,最终的函数一定是形如这样的:
注意到这样函数的复合满足结合律,所以你可以直接在线段树上维护函数,难点主要是怎么合并两个函数
这是我的写法(我对于一个函数维护了它的最小值,最大值和中间那段线的纵截距 \(b\)
friend Kurumi operator^ (Kurumi f, Kurumi g) // Kurumi 是一个函数结构体 (Kurumi 不是狂三是胡桃)
{
Kurumi res;
res.ymx=g.work(f.ymx);
res.ymn=g.work(f.ymn); //结果的最小值一定是这样的
if(res.ymx==res.ymn) { res.b=0; return res; }
int rxmn=f.xmn(); int rxmx=f.xmx();
int t1=f.inv(g.xmn()),t2=f.inv(g.xmx());
if(t1!=-1 and t1>rxmn) rxmn=t1;
if(t2!=-1 and t2<rxmx) rxmx=t2;
res.b=res.ymx-rxmx;
return res;
}
最后代码贴上来:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 3e5+5, inf = 1e9;
int n,q;
struct Kurumi
{
int ymn,ymx,b;
int xmn() { return ymn-b; }
int xmx() { return ymx-b; }
int work(int x)
{
if(x<=xmn()) return ymn;
else if(x>=xmx()) return ymx;
else return x+b;
}
int inv(int y)
{
if(y<ymn or y>ymx) return -1;
else return y-b;
}
void print()
{
printf("ymn=%d ymx=%d b=%d\n",ymn,ymx,b);
}
friend Kurumi operator^ (Kurumi f, Kurumi g)
{
// printf("f: "),f.print();
// printf("g: "),g.print();
Kurumi res;
res.ymx=g.work(f.ymx);
res.ymn=g.work(f.ymn);
if(res.ymx==res.ymn) { res.b=0; return res; }
int rxmn=f.xmn(); int rxmx=f.xmx();
int t1=f.inv(g.xmn()),t2=f.inv(g.xmx());
if(t1!=-1 and t1>rxmn) rxmn=t1;
if(t2!=-1 and t2<rxmx) rxmx=t2;
//assert(rxmx-rxmn==res.ymx-res.ymn);
res.b=res.ymx-rxmx;
return res;
}
} fc[N];
struct Sagiri
{
int l,r;
Kurumi dat;
} t[N<<2];
#define ls (p<<1)
#define rs (p<<1)|1
void build(int p, int l, int r)
{
// printf("%d [%d %d]\n",p,l,r);
t[p].l=l,t[p].r=r;
if(l==r) { t[p].dat=fc[l]; return ; }
int mid=(l+r)>>1;
build(ls,l,mid),build(rs,mid+1,r);
t[p].dat=t[ls].dat^t[rs].dat;
}
void change(int p, int x, const Kurumi &v)
{
if(t[p].l==t[p].r) { t[p].dat=v; return ; }
int mid=(t[p].l+t[p].r)>>1;
if(x<=mid) change(ls,x,v);
else if(x>mid) change(rs,x,v);
t[p].dat=t[ls].dat^t[rs].dat;
}
Kurumi query(int p, int l, int r)
{
if(t[p].l==l and t[p].r==r) return t[p].dat;
int mid=(t[p].l+t[p].r)>>1;
if(r<=mid) return query(ls,l,r);
else if(l>mid) return query(rs,l,r);
else return query(ls,l,mid)^query(rs,mid+1,r);
}
int main()
{
scanf("%d",&n);
for(int i=1,op,w; i<=n; i++)
{
scanf("%d%d",&op,&w);
if(op==1) fc[i]={0,inf,w};
else if(op==2) fc[i]={0,w,0};
else fc[i]={w,inf,0};
}
build(1,1,n);
scanf("%d",&q);
for(int i=1,op,p,w; i<=q; i++)
{
scanf("%d",&op);
if(op==4)
{
scanf("%d",&w);
Kurumi res=query(1,1,n);
printf("%d\n",res.work(w));
}
else
{
scanf("%d%d",&p,&w);
if(op==1) change(1,p,{0,inf,w});
else if(op==2) change(1,p,{0,w,0});
else change(1,p,{w,inf,0});
}
}
return 0;
}
方法2
注意到无论多少次 \(\min, \max, +\) 最终的函数一定是形如 \(\max\{\min\{x+w_1,w_2\},w_3\}\)
两个这种函数也具有可合并性,线段树上维护 \(w_1,w_2,w_3\) 就好啦!
D 积木拼接
题面大意
\(n\) 个蛋糕,每个蛋糕有 \(w_i,h_i\) 。选 \(m\) 个蛋糕满足 \(\sum\limits_{j=1}^mw_{k_j}-\sum\limits_{j=1}^m|h_{k_j}-h_{k_{j+1}}|\) 最大, 因为蛋糕摆成一个环所以 \(k_1=k_{m+1}\),\(n,m\le 2\times 10^5\).
题解
\(O(n^2\log n)\)
注意摆成一个环的话,\(\sum w_{k_j}\) 不会被顺序所影响,所以我们这时候只需要找到 \(\sum\limits_{j=1}^m|h_{k_j}-h_{k_{j+1}}|\) 的最小值.
会发现,一个环上一定会有最小和最大的 \(h_{k_j}\), 所以 \(\sum\limits_{j=1}^m|h_{k_j}-h_{k_{j+1}}|\) 的值一定是大于等于 \(2(\max h_{k_j}-\min h_{k_j})\) 的
也是可以取得到等的,所以就变成求 \(\sum\limits_{j=1}^mw_{k_j}-2(\max h_{k_j}-\min h_{k_j})\) 的最大值了.
按 \(h\) 排序之后,就是一个很好转移的式子.
\(\max\limits_{L=1}^n \max\limits_{R=L+m-1}^n\{ \max \sum\limits_{j=1}^mw_{k_j}-2(h_R-h_L)\}\)
先枚举 \(L\) 再枚举 \(R\) 然后拿个 set 什么的维护一下区间前 \(m\) 大
\(O(n\log^2 n)\)
这东西怎么优化呢,这又是一个小技巧哦
根据红日学长,这题具有决策单调性.
是的决策单调性并不一定是 dp 才会出现哒)φ(>ω<*)
我们通过打表找规律,或者做题经验,或者有做题经验的学长的锐评,可以得知对于每个 \(L\) , 取到最大值的 \(R\) 会满足:
\(R_1\le R_2\le \cdots \le R_n\)
于是我们考虑分治(?红日学长说是一个套路那我就记下来吧!)
怎么分治呢
我们要求 \(L=1, 2, 3, \cdots, n\) 的答案,利用上我们单调性这个性质
我们可以先暴力求出 \(L=mid\) 的时候的答案,也就是枚举 \(R\) ,记录下取到最大值的地方 \(R_{mid}\)
那么我们可以使用可持久化线段树(主席树)求出任意区间前 \(m\) 大之和,这样每一次找 \(ans_{mid}\) 和 \(R_{mid}\) 就是 \(O(n\log n)\) 的.
可持久化线段树的学习笔记 还没补(!!) 补完了(`・ω・´)
然后分治求 \(L\in [1,mid-1]\) 和 \(L\in [mid+1,R]\) 的答案,第二层转移来的 \(R\) 的范围根据单调性就是 \([1,R_{mid}]\) 和 \([R_{mid},n]\)
因为分治最多就 \(\log n\) 层,所以时间复杂度是 \(O(n\log^2n)\) 的
分治每一次就记录一下 \(L\) 的范围和 \(R\) 的范围,具体看这段代码:
void work(int l, int r, int lb, int rb)
{
if(l>r) return ;
int mid=(l+r)>>1;
f[mid]=-inf;
for(int i=max(lb,mid+m-1); i<=rb; i++)
{
ll x=query_sumk(rt[i],rt[mid-1],1,M,m)-((p[i].h-p[mid].h)<<1); //就是那个式子
if(x>f[mid]) f[mid]=x,R[mid]=i; //更新 ans[mid] 和 R[mid]
}
work(l,mid-1,lb,R[mid]),work(mid+1,r,R[mid],rb); // 递归下去找
}
整个的代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e5+5, M = 1e9+5;
const ll inf = 1e18;
int m,n,tot;
struct Misaka { int v,h; } p[N];
int R[N];
ll f[N];
struct Sagiri { int ls,rs,cnt; ll dat; } t[N<<5];
int rt[N];
void change(int &p, int q, ll l, ll r, int x)
{
t[p=++tot]=t[q];
if(l==r) { t[p].cnt++; t[p].dat+=l; return ; }
ll mid=(l+r)>>1;
if(x<=mid) change(t[p].ls,t[q].ls,l,mid,x);
else change(t[p].rs,t[q].rs,mid+1,r,x);
t[p].dat=t[t[p].ls].dat+t[t[p].rs].dat;
t[p].cnt=t[t[p].ls].cnt+t[t[p].rs].cnt;
}
ll query_sumk(int p, int q, ll l, ll r, int k)
{
if(l==r) return (ll)l*k;
ll mid=(l+r)>>1;
int rcnt=t[t[p].rs].cnt-t[t[q].rs].cnt;
if(k<=rcnt) return query_sumk(t[p].rs,t[q].rs,mid+1,r,k);
else return query_sumk(t[p].ls,t[q].ls,l,mid,k-rcnt)+t[t[p].rs].dat-t[t[q].rs].dat;
}
void work(int l, int r, int lb, int rb)
{
if(l>r) return ;
int mid=(l+r)>>1;
f[mid]=-inf;
for(int i=max(lb,mid+m-1); i<=rb; i++)
{
ll x=query_sumk(rt[i],rt[mid-1],1,M,m)-((p[i].h-p[mid].h)<<1);
if(x>f[mid]) f[mid]=x,R[mid]=i;
}
work(l,mid-1,lb,R[mid]),work(mid+1,r,R[mid],rb);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%d%d",&p[i].v,&p[i].h);
sort(p+1,p+1+n, [&] (const Misaka &x, const Misaka &y) { return x.h<y.h; });
for(int i=1; i<=n; i++) change(rt[i],rt[i-1],1,M,p[i].v);
work(1,n-m+1,m,n);
ll res=-inf;
for(int i=1; i<=n-m+1; i++) res=max(res,f[i]);
printf("%lld\n",res);
return 0;
}