codeforces 1080D Olya and magical square (思維+數學)(模擬)

傳送門:codeforces 1080D

題意:給你一個 n 和 k ,表示你現在有一個邊長爲 2^n 的正方形,你需要對其進行恰好 k 次操作,使得:① 左下角和右上角的正方形邊長一樣,假設爲 a;② 存在一條路徑,這條路徑連通左下角和右上角的正方形,並且這條路徑上的所有正方形邊長也都爲 a。其中,操作指的是:找到目前圖形中一個邊長不爲 1 的正方形,假設邊長爲 b,把它變成四個邊長爲 b/2 的小正方形。如果找不到這樣一條路徑,輸出"NO";否則,輸出"YES log2(a)"。如果路徑上的邊長 a 不唯一,隨意輸出一個。

tips:下述變量可能與題意中的變量無關。

思路:假設所選的路徑均爲"左下角->左上角->右上角",即沿着正方形的兩條邊走。那麼我們的思路是先假設路徑長度爲2^(n-1)不斷分割這條路徑以外的正方形,直到除了這條路徑以外沒有正方形可以分割,縮短路徑長度。接下來詳細描述:

因爲題目中給出的 k 是大於等於 1 的,所以最大的輸出答案就是 n-1(即操作一次之後log2(2^(n-1))),如下圖:

那麼除去第一次操作(將初始正方形拆分成上圖所示),我們接下來總的操作次數就是把右下角那個 2^(n-1) * 2^(n-1) 的正方形經過數次操作變爲全都是邊長爲 1 的正方形,假設這個次數爲 s。我們假設正方形長度爲 x,那麼經過分析得出右下角那塊正方形的操作次數 s = 1 + 4 + ... + 4^(n-1) (此處原因讀者可以嘗試自行思考或藉助打表,並不困難)。在這種情況下,顯而易見的是,如果給定的 k <= 1 + s(此處+1是算上第一次操作),那麼就可以直接輸出"YES n-1"。

那如果 k > 1 + s 呢?我們可以把它拆分成下圖所示:

那麼除去前 5 次操作(將初始正方形拆分成上圖所示),我們接下來總的操作次數就是把除了路徑上的7個正方形外的 9 個 2^(n-2) * 2^(n-2) 的正方形經過數次操作變爲全都是邊長爲 1 的正方形,假設這個次數爲 k2。我們已經知道了一個如何將一個小正方形拆分了(但是與之前的公式有所區別),那麼現在的問題是那個 5 次操作和 9 個正方形的 5 和 9 是怎麼得到的。其實也很簡單,如果明白了上一步的操作中 s 的計算方法的話,這裏可以很輕鬆的想出,5 = 1 + 4,假設這一步操作的次數是 k1,那麼 k1 = 1 + 4 + ... + 4^i,其中的 i 表示這次分割已經是第 i 次了。此時我們也可以得到 k2 = 1 + 4 + ... + 4^(n-i)。而那個 9 也不難想出,其實就是總的正方形個數減去路徑上的正方形個數,我們假設這個個數爲block。我們假設一條邊被分爲了 a 段(此處的 a 與題意中的 a 不同),那麼可以得出總的正方形個數爲 a * a ,路徑上的正方形個數爲 2 * a - 1,那麼 block = a * a - 2 * a + 1也不難得出。那麼此時的總結果 s = k1 + block * k2。如果 k <= s,即可輸出"YES n-i",同時我們可以把上一步的 s 計算公式也歸納到這裏面。(這些公式中只需要等比數列的計算就可以得到結果

雖然得到了計算公式,但這樣並不足以解決問題,因爲題目中的 n 的範圍過於龐(e)大(xin),在計算 s 的過程中會爆long long,所以我們需要多進行一些思考。

通過等比數列求和公式我們可以知道:我們計算出的第一個結果爲 s = 1 + ( 4^n - 1 ) / 3,其中最大的值爲 4^n = 2^(2n)。題目中給出的 k 的最大值爲 1e18,同時我們可以得到 2^62 爲 4e18,即 n = 31 是我們計算的極限,而 n > 31 時,我們可以斷言邊長一定是2^(n-1),即輸出"YES n-1"。

這樣子還沒有做完。讀者讀到這裏可以自行嘗試編寫代碼(應該有點感覺了),然後可以到下述代碼中尋找坑點。

AC代碼:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
ll fp(ll a,ll n){
    ll res=1;
    while(n){
        if(n&1) res=res*a;
        a=a*a;
        n>>=1;
    }
    return res;
}
int main(void){
    int t;scanf("%d",&t);
    while(t--){
        ll n,k;scanf("%lld%lld",&n,&k);
        if(n==2&&k==3) printf("NO\n");
        else if(n>31) printf("YES %lld\n",n-1ll);
        else{
            ll a=2;
            int flag=0;
            for(ll i=1;i<=n;i++){
                ll len=n-i;
                ll block=a*a-a*2ll+1ll;
                //printf("%lld\n",block);
                ll k1=(fp(4ll,i)-1ll)/3ll;
                ll k2=(fp(4ll,len)-1ll)/3ll;
                //printf("%lld %lld\n",k1,k2);
                if(k<=k1+k2*block){
                    flag=1;
                    printf("YES %lld\n",len);
                    break;
                }
                //printf("%lld\n",k1+k2*block);
                a*=2ll;
            }
            if(flag==0) printf("NO\n");
        }
    }
    return 0;
}

/*
29 96076792050570581
*/

tips:代碼末尾的數據是 n = 29 時的極限數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章