历届试题 矩阵翻硬币
随后,小明对每一个硬币分别进行一次 Q 操作。
对第x行第y列的硬币进行 Q 操作的定义:将所有第 i*x 行,第 j*y 列的硬币进行翻转。
其中i和j为任意使操作可行的正整数,行号和列号都是从1开始。
当小明对所有硬币都进行了一次 Q 操作后,他发现了一个奇迹——所有硬币均为正面朝上。
小明想知道最开始有多少枚硬币是反面朝上的。于是,他向他的好朋友小M寻求帮助。
聪明的小M告诉小明,只需要对所有硬币再进行一次Q操作,即可恢复到最开始的状态。然而小明很懒,不愿意照做。于是小明希望你给出他更好的方法。帮他计算出答案。
对于20%的数据,n、m <= 10^7;
对于40%的数据,n、m <= 10^15;
对于10%的数据,n、m <= 10^1000(10的1000次方)。
Q操作的实现:
import java.util.Arrays;
public class Main{
// 假设0表示反面
// 小明先把硬币摆成了一个 n 行 m 列的矩阵。
static int[][] qArray = {{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}};
/**
* 随后,小明对“每一个”硬币分别进行 "一次" Q 操作。
* 对第x行第y列的硬币进行 Q 操作的定义:将所有第 i*x 行,第 j*y 列的硬币进行翻转。
*/
private static void qTurn (){
// 其中i和j为任意使操作可行的正整数 > 0,行号和列号都是从1开始。
for( int x = 1; x < qArray.length; x++ ){
for( int y = 1; y < qArray[x].length; y++ ){
for( int i = 1; i < qArray.length/x; i++ ){
for( int j = 1; j < qArray[x].length/y; j++ ){
// 对“每一个”硬币分别进行 "一次" Q 操作
qArray[i * x][j * y] = qArray[i * x][j * y] == 0 ? 1 : 0;
}
}
}
}
}
public static void main( String[] args ){
qTurn();
for(int i = 0; i < qArray.length; ++i){
System.out.println (Arrays.toString (qArray[i]));
}
System.out.println("当小明对所有硬币都进行了一次 Q 操作后,他发现了一个奇迹——所有硬币均为正面朝上。");
System.out.println("小明想知道最开始有多少枚硬币是反面朝上的。" + "于是,他向他的好朋友小M寻求帮助。\n" + "聪明的小M告诉小明,只需要对所有硬币再进行一次Q操作,"
+ "即可恢复到最开始的状态。然而小明很懒,不愿意照做。" + "于是小明希望你给出他更好的方法。帮他计算出答案");
System.out.println("\n由于0表示反面,1表示正面. \n所以: \n");
int count = 0;
for(int i = 0; i < qArray.length; i++ ){
for(int j = 0; j < qArray[i].length; j++){
if(qArray[i][j] == 1){
count++;
}
}
}
System.out.println("最开始有 " + count + "枚硬币是反面朝上的");
}
}
解题思路:
小明提供了一种算法,这里演示一下 n = 2,m = 3 矩阵的翻硬币过程(1 表示 正面, 0 表示反面)
1 | 1 | 1 |
1 | 1 | 1 |
-->(x , y) =(1 , 1)
x的倍数行,y的倍数列要翻转
0 | 0 | 0 |
0 | 0 | 0 |
-->(x , y) =(1 , 2)
x的倍数行,y的倍数列要翻转
0 | 1 | 0 |
0 | 1 | 0 |
-->(x ,y) = (1 , 3)
0 | 1 | 1 |
0 | 1 | 1 |
-->(x , y) = (2 , 1)
0 | 1 | 1 |
1 | 0 | 0 |
-->(x ,y) = (2 , 2)
0 | 1 | 1 |
1 | 1 | 0 |
-->(x ,y) = (2 , 3)
0 | 1 | 1 |
1 | 1 | 1 |
想必大家也看出了规律,我们令初始状态都为1,然后对每一个坐标进行操作,最后值为0个数的就是开始时反面的个数。本想用暴力,但是对于 n 和 m ,如此大的数肯定不行,所以我们继续分析还有什么其他的规律。
先看 n = 1 的情况:对于(1 , m),只要看它翻转的次数的奇偶就能确定它最终的状态(次数为偶数最后肯定为正面,次数为奇数最后肯定为反面)。因为当x = 1, 确定每次第一行都要参与翻转,当 m 是 y 的倍数的时候,(1 , m)就会翻转,所以(1 , m)全过程翻转的次数取决于 m 的约数个数,1 的约数个数为1 , 3 的约数个数为2, 5 的约数个数为2, 9 的约数个数为3。也就是说当 m = k^2 (k = 1 ,2 ,3···) 其约数个数为奇数,否则 其约数个数为偶数。 因为一般数约数都是成对出现,所以必为偶数,而一个数的平方数,有两个约数相等,所以要减一个相同的就变成了奇数。
所以,对于(1 , m)来说,若m = k^2(k = 1 ,2 ,3···)则最终状态为0,否则为1。
而最后0的个数总和就是我们要的结果
最后得出结论:对于n行m列矩阵,经过 Q 操作后 反面的次数 count = sqrt(n) * sqrt(m) ,(即取整后再相乘)。
AC代码:
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
string multiply(string str1,string str2){//字符串相乘函数
string str = ""; //最终结果
int len1 = str1.size(),len2 = str2.size();//计算两个字符串的函数
int result[1000] = {0}; //开辟数组存乘积并初始化
for(int i = len1-1; i >= 0; --i){
for(int j = len2-1; j >= 0; --j){
result[i+j+1] += (str1[i]-'0')*(str2[j]-'0');
}
}
for(int i = len1+len2-1; i >= 1; --i){//让数组的每一位都是正确的数
result[i-1] = result[i-1]+result[i]/10;
result[i] = result[i]%10;
}
int i = 0;
while(result[i] == 0)
++i;//前导零不要
for(int j = i; j < len1+len2; ++j)//转成字符串形式
str += '0'+result[j];
return str;
}
int compare(string str1,string str,int pos){//字符串比较函数
int len1 = str1.size();
int len2 = str.size();
if(len2 > len1+pos)
return 0;
if(len2 < len1+pos)
return 1;
for(int i = 0; i < len2; ++i){
if(str1[i]-'0' > str[i]-'0')
return 1;
if(str1[i]-'0' < str[i]-'0')
return 0;
}
}
string strSqrt(string str){//对字符串开方取整函数
int len = str.size();
string str1 = "",str2 = "";
for(int i = 0; i < (len+1)/2; ++i){//若len为偶数,len/2=(len+1)/2;若len为奇数,len/2+1=(len+1)/2
for(int j = 0; j <= 9; ++j){//因为每一位的数值都是0~9
str1 = str2;
str1 += '0'+j;
if(compare(multiply(str1,str1),str,2*((len+1)/2-1-i)) == 1){
//由于str1后少了(len+1)/2-i-1个0,所以平方以后少了2*((len+1)/2-i-1)个
str2+=j-1+'0';
break;
}
if(j == 9)
str2 += '9';
}
}
return str2;
}
int main(){
string n,m;
cin>>n>>m;
cout<<multiply(strSqrt(n),strSqrt(m))<<endl;
return 0;
}