1. 程式人生 > >藍橋杯_PREV-34_矩陣翻硬幣

藍橋杯_PREV-34_矩陣翻硬幣

問題描述
  小明先把硬幣擺成了一個 n 行 m 列的矩陣。隨後,小明對每一個硬幣分別進行一次 Q 操作。對第x行第y列的硬幣進行 Q 操作的定義:將所有第 i*x 行,第 j*y 列的硬幣進行翻轉。其中i和j為任意使操作可行的正整數,行號和列號都是從1開始。當小明對所有硬幣都進行了一次 Q 操作後,他發現了一個奇蹟——所有硬幣均為正面朝上。
  小明想知道最開始有多少枚硬幣是反面朝上的。於是,他向他的好朋友小M尋求幫助。聰明的小M告訴小明,只需要對所有硬幣再進行一次Q操作,即可恢復到最開始的狀態。然而小明很懶,不願意照做。於是小明希望你給出他更好的方法。幫他計算出答案。
輸入格式


  輸入資料包含一行,兩個正整數 n m,含義見題目描述。
輸出格式
  輸出一個正整數,表示最開始有多少枚硬幣是反面朝上的。
樣例輸入
2 3
樣例輸出
1
資料規模和約定
  對於10%的資料,n、m <= 10^3;
  對於20%的資料,n、m <= 10^7;
  對於40%的資料,n、m <= 10^15;
  對於10%的資料,n、m <= 10^1000(10的1000次方)。

題意:把硬幣擺成n*m的矩陣形式,初始狀態硬幣全部正面朝上,然後對矩陣中每一個元素位置[x][y] (x,y從1開始),迴圈找i,j,若[i*x][j*y] 存在矩陣中,翻轉這個位置對應的硬幣。問這樣的操作後有多少個硬幣反面朝上。

思路:模擬之後打表找規律,設結果為cnt[n,m],z,t為某個正整數,發現當n==1時,有t^2>m>=(t-1)^2,則cnt[1,m] = t -1,即cnt = m開方後向下取整。再將n打表到10左右,發現當m == 1,n變化時,有z^2>n>=(z-1)^2,則cnt[n,1] = (z-1)。即n一定,m變化,首項為(z-1),並且容易看出n一定,cnt有變化時,每一項都相差(z-1),因而得出當n一定時,cnt[n,m]中變化的項是以(z-1)為首項,(z-1)為公差的等差數列,(m變化後cnt有變化時)每次增長的底數即是(t-1),((t-1)變化,cnt才隨之改變)就是說以(t-1)為項數。即cnt[n,m] = a[t-1] = (z-1) + (t-1 -1)*

(z-1)。化簡得 cnt[n,m] = (t-1)*(z-1);程式化公式即為:

cnt[n,m] = floor(sqrt(n))*floor(sqrt(m));

方法:得到公式,題目就簡單很多。但是資料量達到1000位整數,只能用string接收,然後手動實現公式中所需的大整數開方向下取整運算和乘法運算。
1.大整數乘法運算:直接模擬即可。將兩個乘數string 翻轉,方便進位。兩重for迴圈模擬乘法每一位相乘,每一位相乘的結果所在的位置即為當前相乘兩個數的位置數相加之和,即ans[i+j] += big1[i]*big2[j];之後再遍歷ans中每一位,超過10就進位。
2.大整數開方並向下取整運算:假設大整數有len位,若len是奇數,則開方後結果的位數為len/2+1,若是偶數則= len/2;{證明:反向思考,開方反過來說就是兩個相等的數相乘,由乘法運算可得,由於位數相同,最高位為兩數位數-1相加再加1,所以 (len-1)/2+1 可得,若為偶數位說明最高位進位了,那減去那一位即可, 即(len-2)/2+1即 len/2}。開方後結果的位數有了,for迴圈列舉每一位的數字,再進行平方後比較,即可得每一位的數字。這樣得到的string就直接是開方並取整後的結果了。

程式碼實現:

#include<bits/stdc++.h>
using namespace std;
const int MAXW = 2000;

string multiply(string s1,string s2){//大整數乘法
    //翻轉str -> int
    int big1[MAXW],big2[MAXW];
    int len1 = s1.length(), len2 = s2.length();
    for(int i=len1-1;i>=0;i--)
        big1[len1-i-1] = s1[i] - '0';
    for(int i=len2-1;i>=0;i--)
        big2[len2-i-1] = s2[i] - '0';
    //乘法
    int ans[MAXW];
    memset(ans,0,sizeof(ans));
    for(int i=0;i<len1;i++)
        for(int j=0;j<len2;j++)
            ans[i+j] += big1[i]*big2[j];
    //進位
    for(int i=0;i<len1+len2;i++)
        if(ans[i]>=10) {
            ans[i+1] += ans[i]/10;
            ans[i] %= 10;
        }
    //找位數
    int anslength = 1;
    for(int i=len1+len2;i>=0;i--)
        if(ans[i]!=0){
            anslength = i+1;
            break; 
        }
    //get strans
    string strans;
    for(int i=anslength-1;i>=0;i--)
        strans += ans[i] + '0';
    return strans;
}

bool compare(string str1,string str2,int num){//str1 >= str2 的整數返回true
    if(str1.length()<str2.length()+num) //num即為0的個數
        return false;
    if(str1.length()>str2.length()+num) 
        return true;
    else if(str1.length() == str2.length()+num){
        for(int i=0;i<str1.length();i++){
            if(str1[i] < str2[i])
                return false;
            if(str1[i] > str2[i])
                return true;
        }
    }
    return true;
}

string sqrtfloor(string s){//大整數開方向下取整
    int len = s.length();
    if(len&1)//odd
        len = len/2 + 1;
    else len /= 2;  
    string ans = "";
    for(int i=len;i>=1;i--){//結果有len位數
        for(int j=9;j>=0;j--){//每一位有10中可能
            string tmp = ans;
            tmp += j+'0';
            string res = multiply(tmp,tmp);//乘法複雜度很高,湊位數的0沒有必要加進去
            if(compare(s,res,(i-1)*2)){
                ans += j+'0';//找到一位數,加入ans
                break;
            }
        }
    }
    return ans; 
}

int main(int argc, char const *argv[])
{
    string str1,str2;
    cin>>str1>>str2;
    cout<<multiply(sqrtfloor(str1),sqrtfloor(str2))<<endl;
    return 0;
}

小結:這道題做了3個小時,才得了20分。之前沒有想到sqrt,用的是平方數的列舉,這樣string根本實現不了,後來看的題解,才想到開方不是一樣的嗎??????!我當時都方了?!菜,真的菜。希望考試時不會這麼坑。不知道能不能進決賽。
————藍橋杯考試前一週。