T1 密碼
當時想到解法了,卻依然認為自己不會做,我真是個人才。
結論:對於 $\forall i \in [1,n)$ ,滿足密碼不是 $a_i$ 的因數,且密碼是 $a_k$ 的因數,設滿足條件的最小值為 $g$ 則答案為 $\frac{n}{g}$。
一種最好想的做法:列舉 $\gcd(a_k,n)$ 的因數作為 $g$,並列舉 $i \in [1,n)$,判斷是否整除 $a_i$,如果是就不能作為答案。
以上是 60pts 做法,複雜度 $O(f(\gcd(a_k,n)k)$,其中 $f(x)$ 表示 $x$ 的因數個數。
貼個程式碼(60pts):
#define LL long long
LL n,k,a[250005];
LL gcd(LL x,LL y){return !y?x:gcd(y,x%y);}
bool check(LL x){
for(int i=1;i<k;i++)
if(a[i]%x==0)
return 0;
return 1;
}
int main(){
LL g=gcd(n,a[k]),i=1;
for(;i*i<=g;i++) if(g%i==0&&check(i)) return printf("%lld\n",n/i),0;
for(;i>=1;i--) if(g%i==0&&check(g/i)) return printf("%lld\n",n/(g/i)),0;
}
接下來考慮進行最佳化。
已知對於 $a_i$,只有小於 $\gcd(a_k,n)$ 的因數會影響答案判斷。
所以我們可以列舉 $\gcd(a_i,a_k,n)$ 中的質因數,然後搜尋列舉 $m_i$ 的因數(詳見程式碼),記錄下哪些答案是不行的。
中途碰到標記過的數時就直接 return,這樣複雜度可以保證在 $O(k \log V+\sqrt{\gcd(a_k,n)}+f(\gcd(a_k,n)))$,就可以過了。
貼下程式碼(100pts):
#define LL long long
map<LL,bool> mp;
LL n,k,cnt,pr[15],a[250005];
LL gcd(LL x,LL y){return !y?x:gcd(y,x%y);}
void dfs(LL x){
if(mp.count(x)) return;mp[x]=1;
for(int i=1;i<=cnt;i++) dfs(x/pr[i]);
}
int main(){
LL g=gcd(n,a[k]),tmp=g,i=1;
for(i=2;i*i<=tmp;i++){
if(tmp%i==0){
pr[++cnt]=i;
while(tmp%i==0) tmp/=i;
}
}
if(tmp>1) pr[++cnt]=tmp;
for(i=1;i<k;i++) dfs(gcd(g,a[i]));
for(i=1;i*i<=g;i++) if(g%i==0&&!mp[i]) return printf("%lld\n",n/i),0;
for(;i>=1;i--) if(g%i==0&&!mp[g/i]) return printf("%lld\n",n/(g/i)),0;
}
T2 牛牛的猜球遊戲
大水題,用一個字首陣列記錄操作到第 $i$ 項時的杯子的擺放情況。
然後對於每次詢問根據 $a_{l-1}$ 的值得到從 $a_{l-1}$ 執行到 $a_r$ 後的擺放情況
不多說了,直接上程式碼(100pts):
int n,m,a[100005][10],b[10];
int main(){
scanf("%d%d",&n,&m),iota(a[0],a[0]+10,0);
for(int i=1,x,y;i<=n;i++){
copy(a[i-1],a[i-1]+10,a[i]);
scanf("%d%d",&x,&y),swap(a[i][x],a[i][y]);
}
for(int i=1,l,r;i<=m;i++){
scanf("%d%d",&l,&r);
for(int j=0;j<10;j++) b[a[l-1][j]]=j;
for(int j=0;j<10;j++) printf("%d ",b[a[r][j]]);
printf("\n");
}
}
T3 牛牛的湊數遊戲
可以想到一種很簡單的單次 $O(n log n)$ 做法:
將陣列從小到大排序,然後列舉 $i$ 如果 $a_i \leq sum+1$,就把 $a_i$ 加到 $sum$ 中。
根據這種想法可以延伸出一種莫隊的解法,也就是我賽上最開始寫的 30pts 做法:
先按莫隊的板子將所有詢問離線排序,然後我們考慮回滾莫隊。
莫隊時記錄當前的 $ans$ 值,並維護一個小頂堆的優先佇列。
向莫隊中新增元素時,我們就直接將元素塞進優先佇列裡面,然後每次 while 迴圈是否滿足 $top() \leq sum+1$,是就彈出堆頂並累加答案。
這種解法的期望複雜度應該是 $O(n \sqrt{n} \log n)$,但是會被卡,因為回滾時有可能出現每次都重新彈出 $n$ 個數的情況。
所以最壞的複雜度仍為 $O(nm \log n)$,與暴力相同。
但我賽場上沒有想到這個點,一直在死磕莫隊,浪費了多到無法想象的時間
貼一下莫隊的程式碼(30pts):
#define N 100005
#define LL long long
LL tmp,sum,ans[N];
int n,m,cnt,a[N],bel[N];
struct Node{int l,r,num;}s[N];
priority_queue<int,vector<int>,greater<int>> q,tq;
bool cmp(Node x,Node y){
if(bel[x.l]!=bel[y.l]) return bel[x.l]<bel[y.l];
return x.r<y.r;
}
void add(LL &sum,LL x,que &q){
q.push(x);
while(!q.empty()&&sum+1>=q.top()) sum+=q.top(),q.pop();
}
int main(){
scanf("%d%d",&n,&m);
int block=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",a+i),bel[i]=(i-1)/block+1;
for(int i=1,l,r;i<=m;i++){
scanf("%d%d",&l,&r);
if(bel[l]==bel[r]){
sum=0;while(!q.empty()) q.pop();
for(int j=l;j<=r;j++) add(sum,a[j],q);
ans[i]=sum+1;
}
else s[++cnt]={l,r,i};
}
sort(s+1,s+cnt+1,cmp);
for(int i=1,l,r;i<=cnt;i++){
if(bel[s[i].l]!=bel[s[i-1].l]){
r=bel[s[i].l]*block,sum=0;
while(!q.empty()) q.pop();
}
while(r<s[i].r) add(sum,a[++r],q);
l=bel[s[i].l]*block+1,tmp=sum,tq=q;
while(l>s[i].l) add(tmp,a[--l],tq);
ans[s[i].num]=tmp+1;
}
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
}
後來我想通了,但只剩 1h 多一點時間了,雖然說這道題還是做出來了。
莫隊不行於是考慮轉變思路。
能夠想到另外一種單次 $O(n \log V)$ 的解法:
使用樹狀陣列維護對於任意 $x$,滿足小於等於 $x$ 的數字和。
然後 while 迴圈每次加上所有小於等於當前 $sum+1$ 的數,沒有就退出迴圈。
為什麼複雜度是正確的呢?因為如果要加,$sum$ 最少都會翻倍,不加就退出了,自然是 log 的複雜度。
考慮怎麼降低複雜度,有前面莫隊的鋪墊,應該會比較容易想到離線處理所有詢問,至少我死磕莫隊後腦子裡就只剩離線了。
先把所有詢問離線出來然後對於每次 while 迴圈,我們先記錄下上一次迴圈後的答案,並把每次詢問拆成 $[1,l)$ 和 $[1,r]$ 兩個區間,前減後加。
接下來 for 迴圈掃一遍看有沒有詢問的答案發生過變化,沒有就直接 break,複雜度 $O(n \log^2 n)$。
貼一下最後的程式碼(100pts):
#define N 100005
#define LL long long
LL tr[N],lst[N],ans[N];
int n,m,tot,a[N],rk[N];
vector<pair<int,int>> v[N];
void update(int x,int y){while(x<=tot) tr[x]+=y,x+=x&(-x);}
LL query(int x,LL ans=0){while(x) ans+=tr[x],x-=x&(-x);return ans;}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",a+i),rk[i]=a[i];
sort(rk+1,rk+n+1),tot=unique(rk+1,rk+n+1)-rk-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(rk+1,rk+tot+1,a[i])-rk;
for(int i=1,l,r;i<=m;i++){
scanf("%d%d",&l,&r);
v[l-1].push_back({i,-1}),v[r].push_back({i,1});
}
while(1){
for(int i=1;i<=n;i++) lst[i]=ans[i],ans[i]=tr[i]=0;
for(int i=1;i<=n;i++){
update(a[i],rk[a[i]]);
for(auto [x,y]:v[i]) ans[x]+=y*query(upper_bound(rk+1,rk+tot+1,lst[x]+1)-rk-1);
}
for(int i=1;i<=n;i++) if(lst[i]!=ans[i]) goto con;
break;con:;
}
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]+1);
}
T4 牛牛的RPG遊戲
先考慮一維做法,也就是 $\min(n,m) \leq 1$ 的部分分,設 $dp_i$ 表示走到第 $i$ 格的最大得分,得到轉移式:
$
dp_i=max^{j \leq i}_{j=1}dp_j+buff_j*(i-j)+val_j
$
發現是斜率最佳化的經典轉移,使用李超線段樹最佳化即可。
關於我賽時 val 和 buf 輸入反了這一件事,我的 20pts 啊!
一維部分分程式碼(20pts):
#define N 100005
#define LL long long
int n,m;
LL val[N],buf[N],dp[N];
struct Lichao_Segment_Tree{
LL query(LL x)//查詢x位置的最大值
void insert(LL k,LL b)//插入一條直線
}LCT;//李超模板程式碼就不放了
int main(){
for(int i=0;i<m;i++){
if(!i) LCT.insert(0,0);
else{
dp[i]=LCT.query(i)+val[i];
LCT.insert(buf[i],dp[i]-i*buf[i]);
if(val[i]<0) dp[i]-=val[i];
}
}
}
接下來我們考慮擴充套件到二維上。
設 $dp_{i,j}$ 表示走到 $i$ 行 $j$ 列時的最大得分,可以列出轉移式:
$
dp_{i,j}=max^{u\leq i,v\leq j}{u=1,v=1}dp+buff_{i,j}*(u+v-i-j)+val_{i,j}
$
這個時候直接用李超來最佳化就不是很好做了。神犇zyx可以忽略這句話
觀察可以發現 $u \leq i,v \leq j$ 是一個二維偏序,聯想以前學過的知識,cdq 分治可以解決這類問題。
接下來就可以 cdq 套李超線段樹,理論上可能會很毒瘤,但實際寫著其實並不會,時間複雜度 $O(n \log^2 n)$。
貼下程式碼(100pts):
#define N 100005
#define LL long long
int n,m,rt;
vector<LL> val[N],buf[N],dp[N];
struct Lichao_Segment_Tree{
void clear()//清空
LL query(LL x)//查詢x位置的最大值
void insert(LL k,LL b)//插入一條直線
}LCT;//李超模板程式碼就不放了
void cdq(int l,int r){
if(l==r){
LCT.insert(0,0);
for(int i=1;i<=m;i++){
dp[l][i]=max(dp[l][i],LCT.query(i));
LCT.insert(buf[l][i],dp[l][i]-i*buf[l][i]+val[l][i]);
}
return LCT.clear();
}
int mid=(l+r)>>1;cdq(l,mid),LCT.insert(0,0);
for(int j=1;j<=m;j++){
for(int i=l;i<=mid;i++)
LCT.insert(buf[i][j],dp[i][j]-(i+j)*buf[i][j]+val[i][j]);
for(int i=mid+1;i<=r;i++) dp[i][j]=max(dp[i][j],LCT.query(i+j));
}
LCT.clear(),cdq(mid+1,r);
}
多介紹一種來自 zyx 的方法:對每一行開一個李超線段樹,並以 $\min(n,m)$ 作為行,暴力做縱向轉移,時間複雜度 $O(nm \sqrt{nm} \log n)$。