2020牛客暑期多校訓練營(第八場)Enigmatic Partition
阿新 • • 發佈:2020-08-05
2020牛客暑期多校訓練營(第八場)Enigmatic Partition
題目大意:
注意:對於每一個序列,一定要有三個不同的數,且這三個數連續,意思是一定存在 a1,a1+1,a1+2
題解:
先看上面這個圖,這個是對於長度是7,最小數為1的序列對答案的貢獻,很容易發現求貢獻的規律。
對於區間 \([10,18]\) 長度是7,最小數是1,貢獻規律就是:每隔兩個數方案數+1,到一個點之和,每隔一個數方案數-1。這個箭頭向上表示2->3 箭頭向右表示 1->2。
所以很容易推出之前的貢獻規律是對的:
- 因為第一個向上的箭頭肯定是因為有兩個2可以向上走了,如果只有一個是肯定不行的,往上走一次就會消耗掉一個2,所以下次不能馬上向上走,而是要等一格,湊齊兩個2。
- 到一個點之後,一定會向上走,這個是因為1被消耗的只有1個了,無法往右走了,只能往上走,往上走之後也不能往右走了,因為沒有1的生成,所以之候每一次的方案都要-1
找到這個之和,會發現這個就是一個差分陣列,然後求和。
知道這裡,其實這個題目差不多寫完了,之後的二階差分就是一種處理,也可以用線段樹代替。
接下來說說為什麼要用二階差分或者線段樹:
- 首先明確,一定要列舉這個組成的數的長度 \(len\) 和最小的數 \(base\) ,因為這個規律就是建立在這個基礎之上的,再算算這個列舉的複雜度,列舉是 \(O(n)\) 的,最小的數看起來也是 \(O(n)\) 的,但是實際上沒有這麼大,因為每次列舉一個\(base\)
- 對於每次列舉這個 \(len\) 和 \(base\) ,只要處理是一個常數,則這個複雜度肯定是過得去的。
- 每次列舉一個 \(len\) 和 \(base\) ,可以直接求出這個區間的 \(l\) 和 \(r\) 和中間點 \(x\)
- 區間加,這個區間更新其實可以用線段樹或者差分陣列來維護,所以我們就考慮對於本來要求的這個陣列進行一個差分,這樣的話,求一次字首和就可以還原這個陣列。(這個就是二階差分,其實很好理解的,如果還是不太會可以看看程式碼,再自己理解理解)
- 最後要明確對於不同的 \(len\) 和 \(base\) 都可以直接加到這個差分陣列,因為對於一個數可能是由很多不同的 $len $ 或者 \(base\) 構成,這個都不影響方案數,因為方案數是直接累加的。
#include <bits/stdc++.h>
#define debug(x) cout<<"debug:"<<#x<<" = "<<x<<endl;
using namespace std;
typedef long long ll;
const int maxn = 5e5+10;
ll add[maxn],del[maxn],sum[maxn];
int main() {
int n = 1e5;
for (ll len = 3; len <= n; len++) {
for (int base = 1; base * len <= n; base++) {
int left = base * len + 3;
add[left]++;add[left+len-2+len-3+1]--;
del[left+len-2]++;del[left+len-2+len-3+1]--;
}
}
for (int i = 3; i <= n; i++) {
add[i] += add[i - 2];//+差分陣列
del[i] += del[i - 1];//-差分陣列
}
for (int i = 3; i <= n; i++) {
add[i] -= del[i];//差分陣列
}
for (int i = 3; i <= n; i++) {
add[i] += add[i - 1];//方案數
sum[i] = sum[i - 1] + add[i];
}
int T;
scanf("%d", &T);
for (int cas = 1; cas <= T; cas++) {
int l, r;
scanf("%d%d", &l, &r);
printf("Case #%d: %lld\n", cas, sum[r] - sum[l - 1]);
}
}