1. 程式人生 > 實用技巧 >[COCI 2010] OGRADA

[COCI 2010] OGRADA

題目

Description

Matija 的柵欄由N條木板組成,從左到右依次編號為1N。i號木板的高度為hi,每條木板的寬度是1cm。

Matija 想用一個寬度為Xcm的滾筒刷來刷木板。使用滾筒刷時,要保證刷子完全接觸柵欄(不能一部分接觸一部分不接觸);另外,還要保證滾筒平行於地面。因此,每次塗色時,Matija 會在柵欄上選擇連續的x條木板,然後從下往上「刷」,一直刷到這x條木板中最矮者的高度。

根據上述規則,有可能有一些木板沒法用滾筒刷來刷,Matija 不得不用牙刷來「塗」剩下的部分。因此,請幫他求出他最少只需用牙刷「塗」多少平方釐米。他還想知道,在滿足「塗」的面積最少的情況下,他最少要用滾筒刷「刷」多少次。

Input

第一行給出數字N,X.

第二行給出N個數字,代表每個木板的高度.

Output

兩行。
第一行有一個整數,表示 Matija 最少需用牙刷塗多少平方釐米。
第二行有一個整數,表示最少要用滾筒刷「刷」多少次。

Sample Input1

5 3
5 3 4 4 5

Sample Output1

3
2

Sample Input2

10 3
1 7 7 6 7 10 2 1 8 4

Sample Output2

17
5

思路

根據題目 樣例1

$n=5~,k=3$ ;

$a[1]=5~,~a[2]=3~,~a[3]=4~,~a[4]=4~,~a[5]=5$

我們需要滿足題目刷的限制,保證刷子完全接觸柵欄,也就是每次刷的時候不能刷到空的;

那麼對於 $i$ 到 $i+k-1$ 刷的高度就是 $min(a[j])~,~1 \leq j\& \& j \leq i+k-1$ ;

我們設一個 $k$ 陣列把這個高度記下;

$h[i]$ 代表從 $i$ 到 $i+k-1$ 能刷到的高度;

我們做一遍單調佇列就可以求出$min(a[j])~,~1 \leq j\& \& j \leq i+k-1$ ,也就是 $h[i]$ ;

求出後 $h[1]=3$ , $h[2]=3$ , $h[3]=4$ ;

很明顯每條木板都有一個,能刷的最大高度;

如樣例:

$mx[1]=3~,~mx[2]=3~,~mx[3]=4~,~mx[4]=4~,~mx[5]=4$ ;

我們會發現 $mx[3]$ 可以刷到的$h[i]$ 有兩個 $3$ 和 $4$ ;

但是我們要取 $3$ 的 $max(h[i])$ ,也就是取 $4$;

所以$mx[i]=max(h[j])~,~i-k+1 \leq j\& \& j \leq i$ ;

再做一遍單調佇列求出每個 $mx[i]$ 就ok了;

這樣刷不到的地方就是 $\sum ^n _i a[i]-mx[i] $ ;

這樣我們就很輕鬆地解決了第一問;

那麼第二問怎麼求呢?

首先在$i+k-1$ 這個範圍內,如果 $mx[i] \neq mx[j]$ 那麼 ,$ans++$ ;

刷的最大高度不同,則說明在這個範圍內刷了多次;

如樣例

在$a[1]$ 到 $a[3]$ 之間 $mx[1] \neq mx[3]$ ,則這個範圍內刷了$2$ 次;

如果刷的區域超出了上次刷的寬度,那麼說明又刷了一次;

什麼意思呢?

我們來看下面這個特殊的樣例

$n=5~,~k=3$ ,$a[i]=3~,~1 \leq i\& \& i \leq n $;

這個樣例中沒有出現不同的$mx[i]$,但是很明顯也刷了兩次;

第一次從$1$ 刷到了 $3$ ,需要刷的區域 $4$ 超出的這個範圍,說明再刷了一次;

所以如果刷的區域超出了上次刷的寬度,那麼說明又刷了一次;

所以我們可以設一個 $pre$ 表示上次的$mx[i]$ ,設 $pra$ 表示上次刷的寬度,也表示刷到了 $i$;

那麼

if(mx[i]!=pre) //如果刷的高度不同,說明從 i 又刷了一次
{
    ans++;//答案加1
    pra=i+k-1;//記錄下刷到的邊界
}
if(i>pra)//如果超出了這個邊界,說明後來 i 刷了一次
{
    ans++;//統計答案
    pra=i-k+1;//更新邊界
}

這樣統計$ans$ 就ok了;

同樣就輕鬆地解決了第二個問題;

那麼沒有第三個問題了,就直接上程式碼吧;

程式碼

#include<bits/stdc++.h>
#define re register
typedef long long ll;//被坑過一次了,習慣每次都開long long 了
using namespace std;
inline ll read()
{
    ll a=0,f=1; char c=getchar();
    while (c<'0'||c>'9') {if (c=='-') f=-1; c=getchar();}
    while (c>='0'&&c<='9') {a=a*10+c-'0'; c=getchar();}
    return a*f;
}//快讀比 scanf 好打多了
ll n,k;
ll a[1000010],h[1000010],mx[1000010];
ll q[1000010];
int main()
{
    n=read(); k=read();
    for(re ll i=1;i<=n;i++)
        a[i]=read();
    ll head=1,tail=0;//單調佇列初始頭尾
    for(re ll i=1;i<=n;i++)
    {
        while(head<=tail&&i-q[head]+1>k)//如果長度超過刷子寬度 k 
            head++;//    那就踢隊頭
        while(head<=tail&&a[q[tail]]>a[i])//找一個最小值,因為不能刷到空白,所以刷的高度就不能高於木板最小值
            tail--;//    踢隊尾
        q[++tail]=i;//入隊
        if(i-k+1>=0)//下標不為0
            h[i-k+1]=a[q[head]];//記錄
    }
    head=1,tail=0;//重置
    for(re ll i=1;i<=n;i++)
    {
        while(head<=tail&&i-q[head]+1>k)//限制範圍
            head++;//   又踢隊頭
        while(head<=tail&&h[q[tail]]<h[i])//找一個h[i]的最大值
            tail--;//   踢掉,踢掉!!!
        q[++tail]=i;//入隊
        mx[i]=h[q[head]];//再來記錄
    }
    ll sum=0;
    for(re ll i=1;i<=n;i++)//For
        sum+=a[i]-mx[i];
    printf("%lld\n",sum);//輸出第一個問題的答案
    ll ans=0;
    ll pre=0,pra=1<<30;//不需要設為1<<30,只是博主之前用另一種方法寫的時候忘改了
    for(re ll i=1;i<=n;i++)
    {
        if(mx[i]!=pre) //如果刷的高度不同,說明從 i 又刷了一次
        {
            ans++;//答案加1
            pra=i+k-1;//記錄下刷到的邊界
        }
        if(i>pra)//如果超出了這個邊界,說明後來 i 刷了一次
        {
            ans++;//統計答案
            pra=i-k+1;//更新邊界
        }
    }
    printf("%lld\n",ans);//輸出
    //return 0;
}