8.30 上午 becoder 模擬賽總結 & 題解

tkdqmx發表於2024-09-03

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)$。

相關文章