1. 程式人生 > >【清北學堂2018-刷題衝刺】Contest 3

【清北學堂2018-刷題衝刺】Contest 3

 比較數學的一場,難度稍大。

Task 1:數數

【問題描述】

 fadbec 很善於數數,⽐如他會數將a 個紅球,b 個黃球,c 個藍球,d個綠球排成⼀列,求出任意相鄰不同⾊的方案數⽬。

 現在R 君不知道fadbec 數的對不對,想讓你也算⼀算。

 由於數字⽐較⼤,所以請輸出除以109 + 7 的餘數。

【輸入格式】

 ⼀⾏四個正整數a,b,c,d。

【輸出格式】

 輸出包含⼀個整數,表⽰答案。

【樣例輸入1】

1 1 1 2

【樣例輸出1】

36

【資料規模及約定】

 對於前30% 的資料,1 <=a; b; c; d <= 3。

 對於前100% 的資料,1 <= a; b; c; d <= 30。

 直觀想法:暴力搜尋,判斷不同,有30分。

 稍加改造:只有四種球,取s個數的時候對應的有多種不同狀態,按狀態轉移進行DP,注意空間和時間優化,100pts

 比較簡單,不做過多解釋。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ns s&1
#define ls ~s&1
#define lint long long
using namespace std;
const lint modd=1000000007;
lint a,b,c,d,f[2][31][31][31][5];
inline lint mod(lint x){
    return x%modd;
}
inline lint upd(lint s,lint i,lint j,lint k,lint p){
    lint sum=0;
    for(int pp=0;pp<=4;++pp){
        if(pp==p)continue;
        sum=mod(sum+f[s][i][j][k][pp]);
    }
    return mod(sum);
}
int main(){
    freopen("count.in","r",stdin);
    freopen("count.out","w",stdout);
    scanf("%lld%lld%lld%lld",&a,&b,&c,&d);
    f[0][0][0][0][0]=1;
    lint sum=a+b+c+d;
    for(int s=1;s<=sum;++s){
        memset(f[ns],0,sizeof(f[ns]));
        lint maxi=min(a,sum);
        for(int i=0;i<=maxi;++i){
            lint maxj=min(b,sum-i);
            for(int j=0;j<=maxj;++j){
                lint maxk=min(c,sum-i-j);
                for(int k=0;k<=maxk;++k){
                    lint l=s-i-j-k;
                    if(l<0||l>d)continue;
//                  for(int p=1;p<=4;++p){
                        if(i>0)f[ns][i][j][k][1]=mod(f[ns][i][j][k][1]+upd(ls,i-1,j,k,1));
                        if(j>0)f[ns][i][j][k][2]=mod(f[ns][i][j][k][2]+upd(ls,i,j-1,k,2));
                        if(k>0)f[ns][i][j][k][3]=mod(f[ns][i][j][k][3]+upd(ls,i,j,k-1,3));
                        if(l>0)f[ns][i][j][k][4]=mod(f[ns][i][j][k][4]+upd(ls,i,j,k-0,4));
//                  }
//                      printf("s=%lld ns=%lld a=%lld b=%lld c=%lld %lld %lld %lld %lld\n",s,ns,i,j,k,f[ns][i][j][k][1],f[ns][i][j][k][2],f[ns][i][j][k][3],f[ns][i][j][k][4]);
                }
            }
        }
    }
    lint ans=mod(mod(f[sum&1][a][b][c][1]+f[sum&1][a][b][c][2])+mod(f[sum&1][a][b][c][3]+f[sum&1][a][b][c][4]));
    printf("%lld\n",ans);
    return 0;
} 

Task 2:陣列

【問題描述】

 fabdec 有⼀個長度為n 的陣列a[](下標1-n), 初始時都是0。

 fabdec 隨機了⼀個1 到n 的隨機數x,並且把a[x]++。

 fabdec 重複了m 次這樣的操作,然後數了⼀下陣列⾥⼀共有k 個位置為奇數。

  fabdec 現在想問執⾏m 次操作,總共能⽣成多少種不同的陣列使得恰好有k 個位置是奇數?(兩個陣列不同當且僅當兩個陣列存在某個位置陣列的值不相同)

 因為這個數字會很⼤,所以只需輸出這個答案除以109 + 7 的餘數。

【輸入格式】

 ⼀⾏三個整數,n,m,k。

【輸出格式】

  輸出包含⼀個整數,表⽰答案。

【樣例輸入】

2 3 1

【樣例輸出】

4

【資料規模及約定】

 對於前20% 的資料,1 <= n;m <= 4。

 對於前50% 的資料,1 <= n;m <= 2000。

 對於前100% 的資料,1 <= n;m <= 100000, 0 <= k <= n。

 考慮方法:

  •  直接硬搞搜尋,20pts

  •  直接DP,50pts

  •  正解的想法比較巧妙:

  •  奇數的本質其實就是二進位制位下最低位為1,所以k位位奇數,那就在k位上新增上一個1,選擇方法有C(n,k)種。

  •  剩下的數拆分成多個2的形式,隨意分配到每一位上,計算分配型別總數即可。

  •  由於數學不好,我通過打表得到結論:對於ss=(m-k)/2個2可重複地填到n位上,選擇方法有C(n+ss-1,ss)種。

  •  根據乘法原理,答案就是C(n,k)*C(n+ss-1,ss),注意要大力取模。

  •  考慮組合數的計算:因為模數是質數,而且n範圍比較大,考慮線性求階乘逆元。

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #define MAXN 400010
    #define lint long long
    using namespace std;
    const lint mod=1000000007;
    lint n,m,k,fac[MAXN],inv[MAXN];
    //put k into n positions;
    inline lint __pow(lint x,lint y){//x^y
      lint s=1;
      while(y!=0){
          if(y&1){
              y^=1;
              s=x*s%mod;
          }
          y>>=1;
          x=x*x%mod;
      }
      return s;
    }
    void get_fac(lint maxn){
      fac[0]=1;
      for(lint i=1;i<=maxn;++i){
          fac[i]=i*fac[i-1]%mod;
      }
      inv[maxn]=__pow(fac[maxn],mod-2);
    //    printf("inv=%lld\n",inv[n+k]);
      for(lint i=maxn;i>=2;--i){
          inv[i-1]=inv[i]*i%mod;
      }
    }
    inline lint C(lint x,lint y){
      return ((fac[x]*inv[y])%mod)*inv[x-y]%mod;
    }
    int main(){
      freopen("array.in","r",stdin);
      freopen("array.out","w",stdout);
      scanf("%lld%lld%lld",&n,&m,&k);
      if((m-k)&1){
          puts("0");
          return 0;
      }
      lint ss=(m-k)>>1;
      lint maxn=max(max(n+ss-1,ss),max(n,k));
      get_fac(maxn);
    //    for(int i=1;i<=n+k;++i)printf("%d ",fac[i]);puts("");
      lint ans=C(n+ss-1,ss)*C(n,k)%mod;
      printf("%lld",ans);
      return 0;
    }
    

Task 3:子集【問題描述】

 R 君得到了⼀個集合,⾥⾯⼀共有n 個正整數,R 君對這個集合很感興趣,通過努⼒鑽研,發現了這個集合⼀共有2n 個⼦集。

 現在R 君又對這個集合的⼦集很感興趣。

 定義⼀個集合的權值是這個集合內所有數字的和的話,那麼R 君想問問你,這個集合的權值第K ⼩⼦集是多⼤。

 ps. 涉及到較少數字的long long 輸⼊輸出,建議使用cin/cout。

【輸入格式】

 第⼀⾏兩個正整數n,k。

 接下來⼀⾏n 個正整數,表⽰集合內元素。

【輸出格式】

 輸出⼀個數字,表⽰集合的權值第K ⼩⼦集的權值。

【樣例輸入】

2 3        
1 2

【樣例輸出】

2        
6

【資料規模及約定】

  • 對於前20% 的資料,1 <= n <= 15。

  • 對於前40% 的資料,1 <= n <= 22。
  • 對於前100% 的資料,1 <= n <=35, 1 <= k <= 2^n,1 <= 集合元素<=1000000000

 樸素演算法直接dfs構造所有子集,排序求解即可,40pts。

 正解需要使用meet-in-the-middle的思想。正向搜尋搜尋樹的大小是2^35的,是不可接受的。如果兩端同時開始搜尋,就可以讓搜尋樹大小變成2^16+2^15。但是對於中間合併的時候要仔細考慮,避免複雜度退化成2^16^2。所以想到雙向搜尋後,本題的核心問題就成為了怎麼把分開的兩個小集合最終合併成一個大集合。

 這裡我們考慮二分答案,二分第k大集合的數值,判斷該數值不小於的集合數與k的比較。其中判斷函式中的方法類似於“懸線法”的思想,把子集排序後用兩個指標來記錄答案總數。

 順帶一提,預處理拆分的子集構造還有一種更容易寫的方法-使用lowbit構造。但是因為本蒟蒻不會+難理解+實測更慢,在這裡本蒟蒻選擇搜尋構造。

#include<cstdio>
#include<iostream>
#include<algorithm>
#define lint long long
using namespace std;
const int MAXN=(1<<18)+5;
lint n,k,sum,arr[40],cnt_1,cnt_2,s1[MAXN],s2[MAXN];
void dfs_1(lint pos,lint val,lint ed){
    s1[++cnt_1]=val;
    for(lint i=pos;i<=ed;++i){
        dfs_1(i+1,val+arr[i],ed);
    }
}
void dfs_2(lint pos,lint val,lint ed){
    s2[++cnt_2]=val;
    for(lint i=pos;i<=ed;++i){
        dfs_2(i+1,val+arr[i],ed);
    }
}
inline bool judge(lint x){
    lint p1=1,p2=1,ans=0;
    while(s1[p1+1]+s2[p2]<=x&&p1+1<=cnt_1)p1++;
//  printf("p1=%lld p2=%lld\n",p1,p2);
    while(p2<=cnt_2){
        while(s2[p2]+s1[p1]>x&&p1>=0)p1--;
        if(p1<0)break; 
//      printf("ans+=%d\n",p1);
        ans+=p1;
        p2++;
    }
//  printf("x=%lld ans=%lld\n",x,ans);
    return ans>=k;
}
int main(){
    freopen("subset.in","r",stdin);
    freopen("subset.out","w",stdout);
    cin>>n>>k;
    for(int i=1;i<=n;++i){
        cin>>arr[i];
        sum+=arr[i];
    }
    lint bg_1=1,bg_2=n/2+1;
    lint ed_1=n/2,ed_2=n; 
    dfs_1(bg_1,0,ed_1);
    dfs_2(bg_2,0,ed_2); 
    /*
    思路:
        - 把集合分成兩半
        - 預處理這兩個集合的所有子集
        - 二分答案 
    */
    sort(s1+1,s1+1+cnt_1);
    sort(s2+1,s2+1+cnt_2); 
    lint l=1,r=sum;
    while(r>l+1){
        lint mid=(l+r)>>1;
        if(judge(mid)){
            r=mid; 
        }else{
            l=mid;
        }
    }
    printf("%lld",r);
}