在昨天的博客中,我们粗谈了一下二分和一些零碎算法的结合,今天我们讲讲二分和
OI
中另一类非常重要的算法——dp
的结合。
最小间隔 \color{green}{\texttt{最小间隔}} 最小间隔
[Problem] \color{blue}{\texttt{[Problem]}} [Problem]
-
现有 n n n 个数 a 1 ⋯ n a_{1 \cdots n} a1⋯n,你需要从中选取若干个(包括 1 1 1 个),要求如下:
-
- 选取的数和 ≤ m \leq m ≤m( m m m 为题目给定的数)。
-
- 最大间隔最小(间隔:选取数列中后一个数和前一个数在原数列中位置的差 − 1 -1 −1,第一个数的间隔则为它在原数列位置 − 1 -1 −1;最大间隔:所有间隔的最大值)。
-
-
求可能的最小的最大间隔。
-
1 ≤ n ≤ 5 × 1 0 4 , 1 ≤ m ≤ 1 × 1 0 9 1 \leq n \leq 5 \times 10^4,1 \leq m \leq 1 \times 10^9 1≤n≤5×104,1≤m≤1×109。
[Solution] \color{blue}{\texttt{[Solution]}} [Solution]
最大值最小,一看就想到二分答案。
二分答案 mid \texttt{mid} mid,表示最大间隔为 mid \texttt{mid} mid,看看是否可行。
设计一个 dp
:记
f
i
f_{i}
fi 表示第
i
i
i 个数必选时最小的权值和。
我们有如下的转移:
f i = min i − mid − 1 ≤ j ≤ i { f j } + a i f_{i}=\min\limits_{i-\texttt{mid}-1 \leq j \leq i}\left \{f_{j}\right \}+a_i fi=i−mid−1≤j≤imin{fj}+ai
直接转移是
O
(
n
2
)
O(n^2)
O(n2) 的,必超时。仔细观察可以发现,我们可以用单调队列优化我们的 dp
,于是 dp
时间复杂度降为
O
(
n
)
O(n)
O(n)。
总时间复杂度: O ( n × log m ) O(n \times \log m) O(n×logm)。
[code] \color{blue}{\texttt{[code]}} [code]
const int N=5e4+100;
int l,r,mid,ans,n,T;
int f[N],q[N],h,t,a[N];
inline bool check(int mid){
h=1;t=1;q[1]=f[1]=0;//init
// 注意t必须初始化为1而不是0!!
for(int i=1;i<=n;i++){
while (h<=t&&q[h]+mid+1<i) ++h;
f[i]=f[q[h]]+a[i];//转移方程
while (h<=t&&f[q[t]]>=f[i]) t--;
q[++t]=i;//注意入队的是下标而非数
}
for(int i=n-mid;i<=n;i++)
if (f[i]<=T) return true;
return false;
}
int main(){
l=1;r=n=read();T=read();
for(int i=1;i<=n;i++)
a[i]=read();
while (l<=r){
mid=(l+r)>>1;
if (check(mid)){
ans=mid;
r=mid-1;
}
else l=mid+1;
}
printf("%d",ans);
return 0;
}
攻击法坛 \color{green}{\texttt{攻击法坛}} 攻击法坛
[Problem] \color{blue}{\texttt{[Problem]}} [Problem]
- 一条直线上有 n n n 个法坛,分别位于位置 a 1 , a 2 , a 3 ⋯ a n a_1,a_2,a_3 \cdots a_n a1,a2,a3⋯an,你需要把它们全部摧毁。
- 你有两根法杖:一根可以摧毁连续 L L L 个位置上的所有法坛,能使用 R R R 次,另一根能摧毁连续 2 × L 2 \times L 2×L 个位置上的所有法坛,能使用 G G G 次。 L L L 是自己的指定的参数。
- 求 L L L 的最小取值使你能把 n n n 个法坛全部摧毁。
- 1 ≤ n ≤ 2000 , 1 ≤ R , G , a i ≤ 1 × 1 0 9 ( 1 ≤ i ≤ n ) 1 \leq n \leq 2000,1 \leq R,G,a_{i} \leq 1 \times 10^9(1 \leq i \leq n) 1≤n≤2000,1≤R,G,ai≤1×109(1≤i≤n)。
[Solution] \color{blue}{\texttt{[Solution]}} [Solution]
首先明确一点:当 R + G ≥ n R+G\geq n R+G≥n 时,答案为 1 1 1,因为我们可以对于每个法坛都用一次法杖。
明确这点有什么用?有。一特判,二把 R , G R,G R,G 的范围降到 1 ≤ R , G ≤ n 1\leq R,G \leq n 1≤R,G≤n。
答案明显有单调性(可二分性),考虑二分答案。
二分答案 mid \texttt{mid} mid 表示判断当 L = mid L=\texttt{mid} L=mid 时是否可行。
记 P i P_i Pi 表示在 a i a_i ai 位置使用一次第一根法杖最远可以覆盖到哪个法坛,同理 Q i Q_i Qi 表示在 a i a_i ai 位置使用一次第二根法杖最远可以覆盖到哪个法坛。对是否熟悉的同学一读这段话应该就可以知道:我们可以用尺取法在 O ( n ) O(n) O(n) 的时间复杂度内求出 P i , Q i P_i,Q_i Pi,Qi。
特别地,为了方便处理,我们令 P n + 1 = Q n + 1 = n P_{n+1}=Q_{n+1}=n Pn+1=Qn+1=n。
记 f i , j f_{i,j} fi,j 表示使用 i i i 次第一根法杖和 j j j 次第二根法杖最多可以覆盖到前几个法坛,则有转移方程:
f i , j = max ( P f i − 1 , j + 1 , Q f i , j − 1 + 1 ) f_{i,j}=\max(P_{f_{i-1,j}+1},Q_{f_{i,j-1}+1}) fi,j=max(Pfi−1,j+1,Qfi,j−1+1)
转移方程相当的简单易懂。
初始对于 ∀ i , j \forall i,j ∀i,j,都有 f i , j = 0 f_{i,j}=0 fi,j=0 。最后如果 f R , G = n f_{R,G}=n fR,G=n 则 mid \texttt{mid} mid 可行,否则不可行。
时间复杂度: O ( R × G × log a i ) O(R \times G \times \log a_{i}) O(R×G×logai)。
[code] \color{blue}{\texttt{[code]}} [code]
int R,G,n,a[2010],l,r,ans,mid;
int f[2010][2010],P[2010],Q[2010];
inline void ckmax(int &a,int b){
a=max(a,b);//让a取到a,b间较大值
}
inline bool check(int mid){
memset(f,0,sizeof(f));
memset(P,0,sizeof(P));
memset(Q,0,sizeof(Q));
for(int i=1,j=1,k=1;i<=n;i++){
while (j<=n&&a[j]-a[i]+1<=mid) j++;
while (k<=n&&a[k]-a[i]+1<=2*mid) k++;
P[i]=j-1;Q[i]=k-1;//注意这里的减1
}
P[n+1]=Q[n+1]=n;//特判
for(int i=0;i<=R;i++)
for(int j=0;j<=G;j++){
if (i>0) ckmax(f[i][j],P[f[i-1][j]+1]);
if (j>0) ckmax(f[i][j],Q[f[i][j-1]+1]);
}
return f[R][G]>=n;
}
int main(){
scanf("%d%d%d",&n,&R,&G);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
sort(a+1,a+n+1);//先要排序
if (R+G>=n){//足够各个位置都用
printf("1");return 0;
}
l=1;r=a[n]-a[1]+1;
while (l<=r){
mid=(l+r)>>1;
if (check(mid)){
ans=mid;
r=mid-1;
}
else l=mid+1;
}
printf("%d",ans);
return 0;
}