1. 程式人生 > 其它 >藍橋杯2017年省賽[第八屆]-JavaB組賽題解析(下)

藍橋杯2017年省賽[第八屆]-JavaB組賽題解析(下)

技術標籤:藍橋杯藍橋杯數論二分法字首和組合數學

藍橋杯2016年省賽[第七屆]-JavaB組賽題解析
藍橋杯官方講解視訊:https://www.lanqiao.cn/courses/2737
真題文件:https://www.lanqiao.cn/courses/2786/learning/?id=67741

由於篇幅原因,本篇只有7-10題(所有填空題)的解析,1-6題解析見上篇文章
藍橋杯2017年省賽[第八屆]-JavaB組賽題解析(上)


題7.日期問題[19分](★★)

1.題目描述

明正在整理一批歷史文獻。這些歷史文獻中出現了很多日期。小明知道這些日期都在1960年1月1日至2059年12月31日。令小明頭疼的是,這些日期採用的格式非常不統一,有采用年/月/日的,有采用月/日/年的,還有采用日/月/年的。更加麻煩的是,年份也都省略了前兩位,使得文獻上的一個日期,存在很多可能的日期與其對應。


比如02/03/04,可能是2002年03月04日、2004年02月03日或2004年03月02日。

給出一個文獻上的日期,你能幫助小明判斷有哪些可能的日期對其對應嗎?

輸入
一個日期,格式是"AA/BB/CC"。 (0 <= A, B, C <= 9)

輸入
輸出若干個不相同的日期,每個日期一行,格式是"yyyy-MM-dd"。多個日期按從早到晚排列。

樣例輸入
02/03/04

樣例輸出
2002-03-04
2004-02-03
2004-03-02

資源約定:
峰值記憶體消耗(含虛擬機器) < 256M
CPU消耗 < 1000ms

請嚴格按要求輸出,不要畫蛇添足地列印類似:“請您輸入…” 的多餘內容。


所有程式碼放在同一個原始檔中,除錯通過後,拷貝提交該原始碼。
不要使用package語句。不要使用jdk1.7及以上版本的特性。
主類的名字必須是:Main,否則按無效程式碼處理。

2.簡要分析

  • 題目很簡單,主要看能不能考慮周全。
  • 根據題意,我們知道,對於每一個字串都有三種可能構成日期,但是其中會有一些明顯錯誤的日期,如2月31號等,也會有一些重複的日期,如02-02。我們需要做的就是,在這三種可能中排除重複的,錯誤的日期,最後排序輸出。
  • 對於重複和排序,我們可以將日期放入set集合中自動進行去重排序。
  • 對於判斷日期是否有效,我們可以嘗試將日期字串轉換為date型,捕獲異常,沒有異常說明日期是正確的。(注意一定要設定sdf.setLenient(false)
    ,這樣就會對日期做嚴格的限制)

3.實現程式碼

public class _2017_b7 {

    static Set<String> ans=new TreeSet<>();
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        String s=sc.nextLine();
        int a=Integer.valueOf(s.substring(0,2));
        int b=Integer.valueOf(s.substring(3,5));
        int c=Integer.valueOf(s.substring(6,8));
        //三種情況
        check(toYear(a)+"-"+toMonth(b)+"-"+toDay(c));
        check(toYear(c)+"-"+toMonth(a)+"-"+toDay(b));
        check(toYear(c)+"-"+toMonth(b)+"-"+toDay(a));
        for(String ss:ans){
            System.out.println(ss);
        }
    }

    //判斷日期的正確性
    static void check(String str){
        try {
            sdf.setLenient(false);//對日期做嚴格的限制
            sdf.parse(str);
        }catch (Exception e){
            return;
        }
        ans.add(str);
    }

    static String toYear(int num){
        String year= "";
        if(num>=0&&num<=59){
            if(num>=0&&num<9){
                year+="200";
                year+=num;
            }else{
                year+="20";
                year+=num;
            }
        }else{
            year+="19";
            year+=num;
        }
        return year;
    }

    static String toMonth(int num){
        String month="";
        if(num>=1&&num<=9){
            month+="0";
            month+=num;
        }else{
            month+=num;
        }
        return month;
    }

    static String toDay(int num){
        String day="";
        if(num>=1&&num<=9){
            day+="0";
            day+=num;
        }else{
            day+=num;
        }
        return day;
    }

}

題8.包子湊數[21分](★★★★)

1.題目描述

小明幾乎每天早晨都會在一家包子鋪吃早餐。他發現這家包子鋪有N種蒸籠,其中第i種蒸籠恰好能放Ai個包子。每種蒸籠都有非常多籠,可以認為是無限籠。

每當有顧客想買X個包子,賣包子的大叔就會迅速選出若干籠包子來,使得這若干籠中恰好一共有X個包子。比如一共有3種蒸籠,分別能放3、4和5個包子。當顧客想買11個包子時,大叔就會選2籠3個的再加1籠5個的(也可能選出1籠3個的再加2籠4個的)。

當然有時包子大叔無論如何也湊不出顧客想買的數量。比如一共有3種蒸籠,分別能放4、5和6個包子。而顧客想買7個包子時,大叔就湊不出來了。

小明想知道一共有多少種數目是包子大叔湊不出來的。

輸入
第一行包含一個整數N。(1 <= N <= 100)
以下N行每行包含一個整數Ai。(1 <= Ai <= 100)

輸出
一個整數代表答案。如果湊不出的數目有無限多個,輸出INF。

例如,
輸入:
2
4
5
程式應該輸出:
6

再例如,
輸入:
2
4
6
程式應該輸出:
INF

樣例解釋:
對於樣例1,湊不出的數目包括:1, 2, 3, 6, 7, 11。
對於樣例2,所有奇數都湊不出來,所以有無限多個。

資源約定:
峰值記憶體消耗(含虛擬機器) < 256M
CPU消耗 < 1000ms

請嚴格按要求輸出,不要畫蛇添足地列印類似:“請您輸入…” 的多餘內容。

所有程式碼放在同一個原始檔中,除錯通過後,拷貝提交該原始碼。
不要使用package語句。不要使用jdk1.7及以上版本的特性。
主類的名字必須是:Main,否則按無效程式碼處理。
提交程式時,注意選擇所期望的語言型別和編譯器型別。

2.簡要分析

  • 這道題看完後,發現不怎麼看的懂,其實是有兩個點沒有明白。

    • 第一個點,示例1為什麼11 之後所有的數都能表示出來?我怎麼知道多少之後的數一定是能表示出來的呢?
    • 第二個點,什麼情況下是有無窮多數都不能表示的呢?
  • 這上面的兩個點其實就是數學知識,需要對數論的知識有所瞭解才能明白。

  • 關於第一個點的定理:

在這裡插入圖片描述

  • 也就是說如果兩個數互質,那麼不能表示出來的數最大是a*b-a-b,那麼對於題目中的資料來說,如果都互質了,那麼最多不能表示出來的數就是100*100-100-100=9800(取兩個不互質的資料即可算出),所以我們只要驗證9800之前的數能不能表示出來就行了。

  • 關於第二個點的定理:
    在這裡插入圖片描述

  • 也就是說,如果這些數不互質的話,最後使得無解的數會有無窮多種,就要輸出INF。

  • 把上面兩個點弄懂之後,我們只需要正常的求這些資料能不能表示出9800以內的數就行了。

  • 這其實就是個完全揹包問題,dp如下:

  • dp[i]表示第i個數是否能夠湊出來 ,那麼if(dp[j]) dp[j+a[i]]=1;

3.實現程式碼(完全揹包)

public class _08_包子湊數 {

  static int n, g;
  static int[] a = new int[101];
  static boolean[] f = new boolean[10000];

  static int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
  }

  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    n = sc.nextInt();
    f[0] = true;
    for (int i = 1; i <= n; ++i) {
      a[i] = sc.nextInt();

      if (i == 1) g = a[i];//初始化最大公約數
      else g = gcd(a[i], g);
      //完全揹包的遞推
      for (int j = 0; j <= 9800 - a[i]; ++j) {
        if (f[j]) f[j + a[i]] = true;
      }
    }

    if (g != 1) {
      System.out.println("INF");
      return;
    }
    //統計個數
    int ans = 0;
    for (int i = 0; i <= 9800; ++i) {
      if (!f[i]) {
        ans++;
      }
    }
    System.out.println(ans);
  }
}

題9.分巧克力[23分](★★★)

1.題目描述

兒童節那天有K位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友們。
小明一共有N塊巧克力,其中第i塊是Hi x Wi的方格組成的長方形。

為了公平起見,小明需要從這 N 塊巧克力中切出K塊巧克力分給小朋友們。切出的巧克力需要滿足:

  • 1形狀是正方形,邊長是整數
  • 2.大小相同

例如一塊6x5的巧克力可以切出6塊2x2的巧克力或者2塊3x3的巧克力。

當然小朋友們都希望得到的巧克力儘可能大,你能幫小明計算出最大的邊長是多少麼?

輸入 第一行包含兩個整數N和K。(1 <= N, K <= 100000) 以下N行每行包含兩個整數Hi和Wi。(1 <= Hi, Wi <= 100000) 輸入保證每位小朋友至少能獲得一塊1x1的巧克力。
輸出 輸出切出的正方形巧克力最大可能的邊長。

樣例輸入:
2 10
6 5
5 6
樣例輸出: 2

資源約定: 峰值記憶體消耗(含虛擬機器) < 256M CPU消耗 < 1000ms

請嚴格按要求輸出,不要畫蛇添足地列印類似:“請您輸入…” 的多餘內容。
所有程式碼放在同一個原始檔中,除錯通過後,拷貝提交該原始碼。 不要使用package語句。不要使用jdk1.7及以上版本的特性。
主類的名字必須是:Main,否則按無效程式碼處理。

2.簡要分析

  • 題目不是很難,我們可以列舉切巧克力的邊長,初始是所有巧克力中長寬的最大值。從大到小列舉所有可能的巧克力邊長即可。
  • 不過這樣的時間複雜度是O(n*n),題目資料達到了10^5,很明顯會超時,所以我們需要進行優化。
  • 我們可以發現我們列舉邊長的時候是逐次減1的列舉,這樣效率很低,這個問題其實就相當於,從所有可能的長度([1,最長邊長]的連續遞增序列)裡面找一個滿足條件的最大值,我們可以用二分法去進行優化,這樣時間複雜度就是O(n*logn)

3.實現程式碼1(普通列舉)(會超時)

public class _2017_b9 {
    static int n;
    static int k;
    static int[] h;
    static int[] w;
    static int maxLen;//巧克力的最長邊長數
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        n=sc.nextInt();
        k=sc.nextInt();
        h=new int[n];
        w=new int[n];
        for(int i=0;i<n;i++){
            h[i]=sc.nextInt();
            w[i]=sc.nextInt();
            maxLen=Math.max(maxLen,Math.max(h[i],w[i]));
        }
        solve();
    }
    static void solve(){
        //從最大邊長開始嘗試去切巧克力
        int length=n;
        for(;length>=1;length--){
            int count=0;//能夠成功切下來的巧克力數
            //對每塊巧克力嘗試去切
            for(int i=0;i<n;i++){
                count+=(h[i]/length)*(w[i]/length);
            }
            if(count>=k){
                System.out.println(length);
                return;
            }
        }
    }
}

4.實現程式碼2(二分列舉優化)

public class _2017_b9 {
    static int n;
    static int k;
    static int[] h;
    static int[] w;
    static int maxLen;//巧克力的最長邊長數
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        n=sc.nextInt();
        k=sc.nextInt();
        h=new int[n];
        w=new int[n];
        for(int i=0;i<n;i++){
            h[i]=sc.nextInt();
            w[i]=sc.nextInt();
            maxLen=Math.max(maxLen,Math.max(h[i],w[i]));
        }
        solve();
    }
    static void solve(){
        int left=1;
        int right=maxLen;
        int ans=0;
        while(left<=right){
            int mid=(left+right)/2;

            int count=0;//能夠成功切下來的巧克力數
            //對每塊巧克力嘗試去切
            for(int i=0;i<n;i++){
                count+=(h[i]/mid)*(w[i]/mid);
            }

            if(count>=k){
                //要找的值比這個邊長還可以大
                left=mid+1;
                ans=mid;
            }else{
                right=mid-1;
            }

        }
        System.out.println(ans);
    }
}

題10.k倍區間[25分](★★★★)

1.簡要描述

給定一個長度為N的數列,A1, A2, … AN,如果其中一段連續的子序列Ai, Ai+1, … Aj(i <= j)之和是K的倍數,我們就稱這個區間[i, j]是K倍區間。

你能求出數列中總共有多少個K倍區間嗎?

輸入
第一行包含兩個整數N和K。(1 <= N, K <= 100000)
以下N行每行包含一個整數Ai。(1 <= Ai <= 100000)

輸出
輸出一個整數,代表K倍區間的數目。

例如,
輸入:
5 2
1
2
3
4
5

程式應該輸出:
6

資源約定:
峰值記憶體消耗(含虛擬機器) < 256M
CPU消耗 < 2000ms

請嚴格按要求輸出,不要畫蛇添足地列印類似:“請您輸入…” 的多餘內容。

所有程式碼放在同一個原始檔中,除錯通過後,拷貝提交該原始碼。
不要使用package語句。不要使用jdk1.7及以上版本的特性。
主類的名字必須是:Main,否則按無效程式碼處理。

2.簡要分析

  • 題目描述很簡單,第一想法肯定是暴力列舉所有的區間,但仔細一想,遍歷區間和求和,時間複雜度已經到達立方級別了,必然超時了。
  • 但其實最裡面層的迴圈,求和,是可以用字首和陣列優化的,連續子區間的和最好用字首和陣列進行優化。
  • 但是,時間複雜度還是有O(N*N),對於10^5級別的資料,還是會超時。
  • 我們發現,內層迴圈主要做的就是:用字首和陣列計算出區間[i,j]的和。所以我們優化的思路應該放在:如何不列舉每個區間,就能求出每個區間上的和是否是k的倍數。
  • 我們發現,我們列舉區間其實也主要是選定字首和來做差,如果能發現**同餘的數,任意兩個做差,都是k的倍數。**那麼這個題就很好做了。
  • 我們可以用雜湊表將餘數以及出現的次數儲存起來,然後就可以通過排列組合計算出了。如果某一個餘數出現了x次,那麼做差是k的倍數的可能共有x(x-1)/2種
  • 注意雜湊表中要加上(0,1)這組資料,0個的和,餘數是0,初始時也算做出現了一次。

3.實現程式碼1(字首和)(會超時)

public class _2017_b10 {

    static int n;
    static int k;
    static int[] a;
    static long[] pre;//字首和陣列
    static long ans;

    public static void main(String[] args) throws FileNotFoundException {
        Scanner sc=new Scanner(System.in);
        n=sc.nextInt();
        k=sc.nextInt();
        a=new int[n];
        pre=new long[n];
        for(int i=0;i<n;i++){
            a[i]=sc.nextInt();
            pre[i]=(i==0?a[0]:pre[i-1]+a[i]);
        }
        solve();
        System.out.println(ans);

    }
    static void solve(){
        //列舉所有的區間
        for(int i=0;i<n;i++){
            for(int j=i;j<n;j++){
                //區間[i,j]的和
                long sum=(i==0?pre[j]:pre[j]-pre[i-1]);
                if(sum%k==0){
                    ans++;
                }
            }
        }
    }

}

4.實現程式碼2 (同餘法)

public class _2017_b10 {

    static int n;
    static int k;
    static int[] a;
    static int[] pre;//字首和陣列
    static long ans;
    static Map<Integer,Long> map=new HashMap<>();

    public static void main(String[] args) throws FileNotFoundException {
        Scanner sc=new Scanner(System.in);
        n=sc.nextInt();
        k=sc.nextInt();
        a=new int[n+1];
        pre=new int[n+1];
        pre[0]=0;
        map.put(0,1l);
        for(int i=1;i<=n;i++){
            a[i]=sc.nextInt();
            pre[i]=(pre[i-1]+a[i])%k;
            if(map.get(pre[i])==null){
                map.put(pre[i],1l);
            }else{
                map.put(pre[i],map.get(pre[i])+1);
            }
        }
        solve();
        System.out.println(ans);
    }
    static void solve(){
        for(int i=0;i<k;i++){
            if(map.get(i)==null){
                continue;
            }
            long countI=map.get(i);
            ans+=countI*(countI-1)/2;
        }
    }

}


總結

  • 總的來說,題目難度較往年的有所提升。

  • 填空題第3題有坑,第4題很難,程式設計題基本上靠暴力列舉都能獲得一些分數,不過不多。

  • 感覺填空第4題還是有點過於難了。

  • 列舉一下考點:

    • 1.全排列。
    • 2.精度損失的處理。
    • 3.BFS。
    • 4.遞迴。
    • 5.動態規劃。
    • 6.日期處理。
    • 7.完全揹包問題。
    • 8.二分列舉優化。
    • 9.字首和。
    • 10.數學思維。(簡單數論,組合數學)
  • 基本上四個程式設計題程式碼量不是很大,但很考思維,演算法優化,數學知識等。

  • 又是被演算法狂虐的一天。


ATFWUS Writing 2021-1-25