有道小圖靈青少年程式設計精英挑戰活動 中學組 第4場 題解
T1 題解
我們可以發現,到某一個方格距離小於等於某個值的所有方格構成一個斜著的正方形
子任務1
直接每局時間,並字首和處理,可以算出在規定時間內的警察個數
子任務2
二分答案,並沿用子任務1的做法
T2 題解
我們將兩種操作分別簡稱為閱讀/賣出。
子任務1:
暴力列舉操作序列。暴力列舉每次是賣出還是閱讀。
時間複雜度:\(O(q*n!*2^n)\)
子任務2:
考慮如何處理單次詢問。考慮貪心。
首先,每次選擇閱讀時,必定會選擇當前能選擇的書當中最便宜的那個。每次賣出書籍時,由於賣出書籍得到的收益與被賣出書籍的價格無關,所以選擇當前能選擇的書當中最貴的那個必定最優,因為這樣我們就可以把更便宜的書留個以後的購買。
因此我們可以暴力列舉每一天究竟是賣出還是閱讀,當確定了是賣出還是閱讀後,我們就可以唯一確定操作物件,這樣就省去了列舉操作物件的序列。
時間複雜度:\(O(q*2^n)\)
子任務3:
首先我們將\(p\)陣列從小到大排序。
假設我們已經決定了在\(m\)次操作中閱讀\(k\)本書。
注意到,我們會盡量想推遲賣出一本書的時間,因為賣出的越晚,賣出時閱讀過的書籍數量越多,可以拿到的錢就更多。
因此,最優的策略一定是,首先賣出儘量少的書籍湊夠購買第\(1\)本書的錢,然後閱讀第一本書,再賣出儘量少的書籍湊夠購買第\(2\)本書的書籍,再閱讀第\(2\)本書,以此類推,直到購買完\(k\)本書籍,再在餘下回合中棄掉。
列舉k,我們可以得到一個時間複雜度為\(O(q*n^2)\)的演算法。
接下來我們考慮優化這個貪心。
設我們原本希望購買\(k\)本書,現在我們打算賣掉第\(k+1\)本書,假設此時\(k+1\)會給我們帶來\(k+2\)個金幣;但假如我們決定閱讀第\(k+1\)本書而不是把它賣掉,我們將花費$p _ {k+1} \(個金幣,但我們會多獲得等於剩餘卡牌數量的金幣,設為t。因此我們只需要判斷若\)k+2 \leq -p _ { k+1 } + t\(則可以取\)k+1$。
時間複雜度:\(O(nq)\)
子任務4:
發現\(k\)隨\(m\)的增長是單調遞增的。可以用這種理解方式,即書的總數量越多,閱讀一本書的收益就越大,因此閱讀的書至少不減。
於是我們可以從前往後掃,並維護\(k\)的指標,每次判斷能不能後移,最多後移\(n\)次。
時間複雜度:\(O(n\log n+q\log q)\),\(\log\)來自排序。
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=200005;
typedef long long ll;
ll n;
ll a[maxn];
struct Query
{
int m;
int id;
}t[maxn];
bool cmp(Query x, Query y)
{
if(x.m==y.m) return x.id<y.id;
else return x.m<y.m;
}
ll ans[maxn];
ll cel(ll x, ll y)
{
if(x%y==0) return x/y;
else return x/y+1;
}
int main()
{
// freopen("card20.in","r",stdin);
// freopen("card20.out","w",stdout);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+n+1);
int q; cin>>q;
for(int i=1;i<=q;i++)
{
cin>>t[i].m;
t[i].id=i;
}
sort(t+1,t+q+1,cmp);
ll p=0, lst=0;
ll prof=0;
ll pbf=0;
t[0].m=0;
for(int i=1;i<=q;i++)
{
prof+=(t[i].m-t[i-1].m)*(2+p);
int m=t[i].m,req,te,delt;
while(p<n)
{
te=p+1;
req=(pbf>=a[te]?0:(cel((a[te]-pbf),(2+p))));
if(req+lst+p+1>n) break;
delt=-a[te]-(2+p)+(t[i].m-p-1-req-lst);
// cout<<"queue "<<p<<' '<<req<<' '<<delt<<' '<<-a[te]-(2+p)<<' '<<(t[i].m-p-1-req-lst)<<' '<<lst<<' '<<req<<' '<<t[i].m<<endl;
if(delt>=0)
{
pbf+=((2+p)*req-a[te]);
p++;
lst+=req;
prof+=delt;
}
else break;
}
ans[t[i].id]=prof;
}
for(int i=1;i<=q;i++) cout<<ans[i]<<endl;
return 0;
}
T3 題解
首先來進行一步轉換,把“這種餅乾第一次出現的下標”轉換成“這種餅乾是第幾個出現的”(個人認為比較好處理,也方便理解)
由於這題給出一個數組求第幾個出現,所以很自然地想到可以用類似數位DP的套路來做
那麼令 \(dp_{i,j,0/1}\) 代表遍歷到第 \(i\) 個位置,出現了 \(j\) 中餅乾,有沒有首位限制的數列方案數(\(0\) 代表沒有限制,\(1\) 代表有)
那麼轉移如下:
\(dp_{i,j,0} * j \rightarrow dp_{i+1,j,0}\)
\(dp_{i,j,1} * (a_{i+1}-1) \rightarrow dp_{i+1,j,0}\)(不管 \(a_{i+1}\) 是否比 \(j\) 大,在這裡都不能取)
\(dp_{i,j,0} \rightarrow dp_{i+1,j+1,0}\)
考慮有限制的情況
如果 \(a_{i+1} > j\)
\(dp_{i,j,1} \rightarrow dp_{i+1,j+1,1}\)
否則 \(dp_{i,j,1} \rightarrow dp_{i+1,j,1}\)
初始化 \(dp_{1,1,1}=1\),因為顯然第一個位置只能是 \(1\)
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef long long ll;
ll read()
{
char c = getchar(); ll ans = 0, f = 1;
while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9') {ans = ans * 10 + c - '0'; c = getchar();}
return ans * f;
}
const int INF = 1e9;
const int MAXN = 5010;
const int MOD = 1e9 + 7;
ll dp[2][MAXN][2], b[MAXN], a[MAXN];
ll n, ans = 1, cnt;
inline ll get(ll x) {return (x >= MOD) ? (x - MOD) : ((x < 0) ? (x + MOD) : x);}
inline ll add(ll a, ll b) {return get(a + b);}
inline ll sub(ll a, ll b) {return get(a - b);}
inline ll mul(ll a, ll b) {return a * b % MOD;}
int main()
{
n = read();
for(int i = 1; i <= n; ++ i) b[i] = read();
for(int i = 1; i <= n; ++ i)
{
if(b[i] == i) a[i] = ++ cnt;
else a[i] = a[b[i]];
}
dp[1][1][1] = 1;
for(int i = 1; i < n; ++ i)
{
int cur = i & 1, nxt = (i + 1) & 1;
for(int j = 1; j <= i; ++ j)
{
dp[nxt][j][0] = add(dp[nxt][j][0], add(mul(dp[cur][j][0], j), mul(dp[cur][j][1], a[i + 1] - 1)));
dp[nxt][j + 1][0] = add(dp[nxt][j + 1][0], dp[cur][j][0]);
if(a[i + 1] <= j) dp[nxt][j][1] = add(dp[nxt][j][1], dp[cur][j][1]);
else dp[nxt][j + 1][1] = add(dp[nxt][j + 1][1], dp[cur][j][1]);
dp[cur][j][0] = dp[cur][j][1] = 0;
}
}
for(int i = 1; i <= n; ++ i) ans = add(ans, dp[n & 1][i][0]);
printf("%lld\n", ans);
return 0;
}
T4 題解
前置知識:線段樹(部分分)、單調棧、單調佇列、STL set 的使用、重構思想。
無解顯然是存在一個值 \(>Y\) 的數。
子任務 1:
肯定能把整個分成一段,答案就是所有權值和。
子任務 2:
設 \(s_i\) 為 \(A\) 的字首和。
考慮 DP,\(f_{i}\) 表示把 \(1 \sim i\) 分成若干段最優解,轉移為 \(\displaystyle f_i = \min_{j<i, i - j \le X, s_i - s_j \le Y}\{f_j + \max_{k=j+1}^i(A_k)\}\)
暴力 DP 是 \(O(n^3)\) 的。
子任務 3:
上一步用個線段樹搞搞?
子任務 4:
裡面的 \(\max\) 用 ST 表預處理一下,或者第二維倒著掃,動態更新那坨,都是 \(O(n^2)\) 的。
子任務 5:
留給不知道是啥的。。
子任務 6:
考慮用資料結構暴力優化 DP。
首先對於一個 \(i\),合法的決策 \(j\) 是個區間,可以雙指標線性獲得。
設 \(f_j + \max_{k=j+1}^i(A_k)\) 東西,設 \(c_j\) 等於這個東西,再設 \(w_j = \max_{k=j+1}^i(A_k)\)。
即 \(c_j = f_j + w_j\)。考慮用線段樹動態維護這個 \(c\) 陣列,每次 DP 取合法決策區間的最大值即可。
\(f\) 插入就不變了,考慮 \(w\) 如何變,加入 \(A_i\) 後,設 \(p_i\) 為在 \(i\) 左側第一個比 \(A_i\) 大的數(這個可以線性單調棧),\(A_i\) 影響的範圍即 \([p_i, i - 1]\),線上段樹上區間覆蓋即可。
時間複雜度 \(O(n \log n)\) 但由於常數比較大貌似過不了 \(n = 10^6\)。(可能也可以過,不太清楚)
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;
#define MAXN 3000100
#define ll long long
inline ll read()
{
ll x=0;char c=getchar();
while (c<'0'||c>'9') c=getchar();
while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
return x;
}
ll n, m, cnt, a[MAXN], q[MAXN], fi[MAXN], len, Sum, sum[MAXN], dp[MAXN], now;
struct node
{
ll left, right, lazy, Min;
node *ch[2];
}pool[4*MAXN], *root;
inline void buildtree(node *p, ll left, ll right)
{
p->left = left; p->right = right; p->Min = 1000000000;
if(left == right) return ;
ll mid = (left + right) / 2;
node *lson = &pool[++cnt], *rson = &pool[++cnt];
p->ch[0] = lson; p->ch[1] = rson;
buildtree(lson, left, mid); buildtree(rson, mid+1, right);
}
inline void push(node *p)
{
if(p->lazy == 0) return ;
if(p->ch[0]) p->ch[0]->lazy += p->lazy;
if(p->ch[1]) p->ch[1]->lazy += p->lazy;
p->Min += p->lazy;
p->lazy = 0;
}
inline void change1(node *p, ll left, ll right, ll s)
{
if(p->left == left && p->right == right)
{
p->lazy += s;
return ;
}
if(p->ch[0]->right >= right) change1(p->ch[0], left, right, s);
else if(p->ch[1]->left <= left) change1(p->ch[1], left, right, s);
else
change1(p->ch[0], left, p->ch[0]->right, s), change1(p->ch[1], p->ch[1]->left, right, s);
if(p->ch[0]) push(p->ch[0]);
if(p->ch[1]) push(p->ch[1]);
p->Min = min(p->ch[0]->Min, p->ch[1]->Min);
}
inline void change2(node *p, ll left, ll right, ll s)
{
if(p->left == left && p->right == right)
{
p->Min = s;
return ;
}
if(p->ch[0]->right >= right) change2(p->ch[0], left, right, s);
else if(p->ch[1]->left <= left) change2(p->ch[1], left, right, s);
p->Min = min(p->ch[0]->Min, p->ch[1]->Min);
}
inline ll query(node *p, ll left, ll right)
{
push(p);
if(p->left == left && p->right == right) return p->Min;
if(p->ch[0]->right >= right) return query(p->ch[0], left, right);
else if(p->ch[1]->left <= left) return query(p->ch[1], left, right);
else
return min(query(p->ch[0], left, p->ch[0]->right), query(p->ch[1], p->ch[1]->left, right));
}
void dfs(node *p)
{
push(p);
if(p->left == p->right)
{
printf("%lld ",p->Min);
return ;
}
dfs(p->ch[0]);
dfs(p->ch[1]);
}
int main()
{
scanf("%lld%lld%lld",&n,&len,&Sum);
ll mx=0, u, v, first, last;
for(ll i=1;i<=n;i++) a[i] = read(), mx = max(mx, a[i]), sum[i] = sum[i-1] + a[i];
if(mx > Sum)
{
printf("No Solution\n");
return 0;
}
dp[1] = a[1];
q[1] = 1;
root = &pool[++cnt];
buildtree(root, 0, n);
change2(root, 0, 0, 0);
first = 1; last = 0;
for(ll i=1;i<=n;i++)
{
while(sum[i] - sum[now] > Sum || i-now > len) now++;
q[++last] = i;
fi[i] = i-1;
change1(root, i-1, i-1, a[i]);
while(first < last && a[q[last-1]] < a[q[last]])
{
change1(root, fi[q[last-1]], fi[q[last]]-1, a[q[last]]-a[q[last-1]]);
fi[q[last]] = fi[q[last-1]];
q[last-1] = q[last];
last--;
}
// dfs(root);
dp[i] = query(root, now, i-1);
change2(root, i, i, dp[i]);
// cout<<endl;
}
// for(ll i=1;i<=n;i++) printf("%lld ",dp[i]);
printf("%lld\n",dp[n]);
return 0;
}
子任務 7
能資料結構硬優化的已經儘可能優化了,沒有什麼優化空間。
考慮分析出一些性質,我們排除一些不必要的決策。
首先由一個性質 \(f_{i-1} \le f_{i}\) ,比較顯然,明顯把最後一段最後一個數去掉答案不會增加。
著眼於最後一段,考慮這個決策 \(j\),如果這個 \(A_j\) 加入不會把 \(w\) 這個東西改變,也就他不是 \([j, i]\) 的最大值,並且加入這個數最後一段不會超出長度、和的限制,那麼就可以把這個納入最後一段,這個決策就不如 \(j - 1\)。
因此,一個決策 \(j\) 可能成為 \(f_i\) 的最優決策僅當:
- \(A_j\) 是 \(A_{j \sim i}\) 的最大值。
- \(j\) 是可行的決策區間最靠左的一個。
第二個決策只有一個暴力轉移即可。
考慮第一個決策,事實上就是維護一個 \(A\) 單調遞減的單調佇列(為了保持選的段滿足長度限制還得踢出隊頭)。
考慮在插入 \(A_{1 \sim i}\) 進單調佇列後,單調佇列除了最後一項外,都是可能的決策點。考慮他們的 \(w\) 是什麼,你可以 \(\text{ST}\) 表 \(O(1)\) 求,也可以分析性質,此時一個決策對應的 \(w\) 就是單調佇列的上的下一個位置的 \(A\) 值。
考慮轉移,你需要知道這些決策對應的 \(c\) 值的最大值,因此你需要支援一個這樣一個雙端插入刪除求最值的資料結構,操作 \(O(n)\) 次:
- 尾部插入
- 首尾刪除
- 求最大值
可以再開一個 \(\text{multiset}\),插入刪除對應在這個上面操作即可。這樣是單次 \(\log n\) 的。
時間複雜度還是 \(O(n \log n)\)。
事實上由於每次維護的決策的佇列都不滿,所以卡不滿,可以獲得 \(80\) 分。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <set>
#define int long long
using namespace std;
typedef long long LL;
const int N = 3000005;
int n, a[N], q[N];
LL m, t, s[N], f[N];
multiset<LL> st;
LL inline g(int x) { return f[q[x]] + a[q[x + 1]]; }
void inline read(int &x) {
x = 0; char s = getchar();
while (s < '0' || s > '9') s = getchar();
while (s <= '9' && s >= '0') x = (x << 1) + (x << 3) + (s ^ 48), s = getchar();
}
signed main() {
scanf("%lld%lld%lld", &n, &t, &m);
for (int i = 1; i <= n; i++) {
read(a[i]), s[i] = s[i - 1] + a[i];
if (a[i] > m) { puts("No Solution"); return 0; }
}
int j = 0, hh = 1, tt = 0;
for (int i = 1; i <= n; i++) {
while (j + 1 < i && (s[i] - s[j] > m || i - j > t)) j++;
while (hh < tt && (s[i] - s[q[hh]] > m || i - q[hh] > t)) st.erase(st.find(g(hh++)));
while (hh <= tt && a[q[tt]] <= a[i]) {
if (hh < tt) st.erase(st.find(g(tt - 1)));
tt--;
}
q[++tt] = i;
if (hh < tt) st.insert(g(tt - 1));
f[i] = f[j] + a[q[hh]];
if (hh < tt) f[i] = min(f[i], *st.begin());
}
printf("%lld\n", f[n]);
return 0;
}
子任務 8
考慮子任務 \(7\) 所說的資料結構。如果單端刪除我們可以用單調佇列做到線性,這是因為我們建立了一個時間刪除的先後順序讓我們可以踢掉”比你小還比你強“這樣的東西使其單調,那雙端插入刪除能不能做到呢?
這裡引入一個不知道叫啥的資料結構,就叫雙調棧吧,他可以支援均攤 \(O(1)\) 每次兩端插入刪除求最值:
- 考慮設定一箇中點 \(mid\),假設每次刪除不會越過 \(mid\),左右維護兩個棧。順著這個假設,如果說我比你靠近中間,還比你小,那麼你就沒用了,就可以不插入。這樣就做到了讓兩個棧兩端小,中間大做到單調。
- 如果刪除越過 \(mid\),你就考慮把當前存在的元素都暴力重構,\(mid\) 還是取中位數。
這樣複雜度為啥是對的呢?考慮一次暴力重構,意味從上次重構到現在,有著 \(\frac{mid}{2}\) 量級,也就是和 \(mid\) 同階的數遭到了刪除,你的重構複雜度與這個同階。而總共刪除的數是 \(O(n)\) 的,因此可以做到總共複雜度線性 \(O(n)\)。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <set>
using namespace std;
typedef long long LL;
const int N = 3000005;
int n, a[N], q[N], mid, hh = 1, tt;
LL m, t, s[N], f[N], A[N], B[N], t1, t2, val[N];
LL inline g(int x) { return f[q[x]] + a[q[x + 1]]; }
void inline read(int &x) {
x = 0; char s = getchar();
while (s < '0' || s > '9') s = getchar();
while (s <= '9' && s >= '0') x = (x << 1) + (x << 3) + (s ^ 48), s = getchar();
}
void inline pushA(int x) {
if (!t1 || val[A[t1]] > val[x]) A[++t1] = x;
}
void inline pushB(int x) {
if (!t2 || val[B[t2]] > val[x]) B[++t2] = x;
}
void inline rebuild() {
t1 = t2 = 0;
mid = (hh + tt) >> 1;
for (int i = mid; i >= hh; i--) pushA(i);
for (int i = mid + 1; i < tt; i++) pushB(i);
}
int main() {
scanf("%d%d%lld", &n, &t, &m);
for (int i = 1; i <= n; i++) {
read(a[i]), s[i] = s[i - 1] + a[i];
if (a[i] > m) { puts("No Solution"); return 0; }
}
int j = 0;
for (int i = 1; i <= n; i++) {
while (j + 1 < i && (s[i] - s[j] > m || i - j > t)) j++;
while (hh < tt && (s[i] - s[q[hh]] > m || i - q[hh] > t)) {
if (t1 && A[t1] == hh) t1--;
hh++;
if (hh >= mid) rebuild();
}
while (hh <= tt && a[q[tt]] <= a[i]) {
if (t2 && B[t2] == tt - 1) t2--;
tt--;
if (tt <= mid) rebuild();
}
q[++tt] = i;
if (hh < tt) {
val[tt - 1] = g(tt - 1);
pushB(tt - 1);
}
f[i] = f[j] + a[q[hh]];
if (t1) f[i] = min(f[i], val[A[t1]]);
if (t2) f[i] = min(f[i], val[B[t2]]);
}
printf("%lld\n", f[n]);
return 0;
}