【总结向】PAT甲级 简单数学 20200525
引:本周值得关注的题目
- 1049 1的个数:求1-N中数字1在每一位上出现的个数,可以通过枚举N的每一位通过排列组合计算1出现的次数,累加得到答案,暴力枚举会超时。
- 1059 质因子:列举质因子时,为了减少复杂度,只列举前根号n的因子,循环结束后,可能n剩余1,可能剩余最大的>根号n的因子,这时输出>根号n的因子即可
- 1081 有理数的和:在做分数通分时候,先除后乘可有效减少溢出问题
- 1103 整数N的K-P分解:给出了两种思路DFS和DP,这题可以转化为二维完全背包问题来做
一、重点题目
1.1049 Counting Ones (30分)
思🦌:
N=1e9,暴力的话,要从1到1e9每个数判断每一位(最多10次),则复杂度为1e10。因此将问题转化为从1到N,1在[每一位]出现的次数。然后遍历N的位数得到答案,以降低复杂度
不妨设,N=abcdefg,现讨论[1,abcdefg]区间,1在第四位(d)出现的次数.我们使用排列组合的思想
- d=0时,[1,abc0efg]中,要想出现第四位为1的数Ni.那么Ni的aibici一定是在0到abc-1中的,eifigi可以取0-999.故次数为abc*1000
- d=1时,[1,abc1efg]中,要想出现第四位为1的数Ni.需要再分两种情况
- d=1时,当Ni在[abc0000,abc1def]中,要想出现第四位为1的数Ni.那么Ni的aibici一定是abc,eifigi可以取0-eifigi.故次数为efg+1
- d=1时,当Ni在[1,abc0000)中,要想出现第四位为1的数Ni.那么Ni的aibici一定是在0到abc-1中的,eifigi可以取0-999.故次数为abc*1000
- d>1时,当Ni在[1,abcdefg]中,要想出现第四位为1的数Ni,那么Ni的aibici一定是在0到abc中的,eifigi可以取0-999,故次数为(abc+1)*1000
维护高位数hi,存储abc, 低位数lo,存储efg. 遍历N的每一位得到结果
代🐎:
// 1049 Counting Ones
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main()
{
string n;
cin >> n;
int re = 0;
for(int i = n.size() - 1; i >= 0; i --)
{
int lo = 0, hi = 0, base = 1;
//计算lo,hi
for(int j = 0; j < n.size(); j ++){
if(j > i) lo = lo * 10 + (n[j] - '0'), base *= 10;
if(j < i) hi = hi * 10 + (n[j] - '0');
}
//cout << lo << " " << hi << endl;
if(n[i] == '0') re += hi * base;
else if(n[i] == '1') re += hi * base + 1 + lo;
else re += (hi + 1) * base;
}
cout << re << endl;
return 0;
}
2.1059 Prime Factors (25分)
思🦌:
素数打表,然后采用试除法:如果N可以整除该质数,那么再试一次,直到N除尽为1.
循环为不断增加的质数,最多40000个,注意,因子可以大于根号n,但只有1个唯一的一个因子>n
因此我们列举根号n前的质数,只剩>根号n的因子或1退出循环。最后如果有,输出最大的因子
代🐎:
//1059 Prime Factors
#include<iostream>
#include<cstring>
using namespace std;
const int N = 40000;
bool p[N]; // 素数表
void init()
{
memset(p, true, sizeof p);
int psize = 40000;
for(int i = 2; i <= psize; i++)
{
if(p[i]){
for(int j = i + i; j <= psize; j += i)
p[j] = false;
}
}
}
int main()
{
init();
int n;
cin >> n;
bool isFirst = true;
if(n == 1){ // 特判
cout << n << "=" << n << endl;
return 0;
}
else cout << n << "=";
for(int i = 2; i <= n / i; i ++)
{
if(!p[i]) continue;
int cnt = 0;
while(n % i == 0) cnt ++, n /= i;
if(cnt == 0) continue;
if(isFirst) isFirst = false; //第一个符号不输出*
else cout << "*";
cout << i ;
if(cnt > 1) cout << "^" << cnt;
}
if(n > 1){
if(!isFirst) cout << "*";
cout << n << endl;
}
return 0;
}
3.1081 Rational Sum (20point(s))
思🦌:
这个题要注意数据溢出的问题,先除再乘,分数加法通分时要通分时,如8/14+2/6,公因子是42而非84。这样可以有效减少溢出
代🐎:
// 1081 Rational Sum (20point(s))
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 110;
LL gcd(LL a, LL b)
{
return b ? gcd(b, a % b) : a;
}
int main()
{
int n;
cin >> n;
LL a = 0, b = 1;
while(n --)
{
LL c, d;
scanf("%lld/%lld", &c, &d);
LL e = gcd(c, d);
c /= e, d /= e;
LL p = gcd(b, d); //再通分时候,要先约分再通分,否则会溢出
a = d / p * a + b / p * c;
b = b / p * d; // 先除再乘,防止溢出
LL f = gcd(a, b);
a = a / f;
b = b / f;
}
LL g = a / b;
LL h = a % b;
if(b < 0) b = -b, a = -a;
if(!g && !h) cout << "0"; // 0
if(g && !h) cout << g; //只有整数
if(g && h) cout << g << " "; // 存在整数和小数
if(h) cout << a % b << "/" << b; //有小数
return 0;
}
6.1103 Integer Factorization (30point(s))
思🦌:
这个题要注意数据溢出的问题,先除再乘,分数加法通分时要通分时,如8/14+2/6,公因子是42而非84。这样可以有效减少溢出
这题用dfs可能会超时,dp可以将时间缩短40倍(680ms VS 17ms),本题可以转化为二维完全背包问题来做
f
(
m
,
i
,
j
)
f(m,i,j)
f(m,i,j)表示选取前m个物品,背包容量N=i,格子个数k=j的集合中,背包最大的价值(因子和)
因此二维完全背包问题有
f
(
m
,
i
,
j
)
=
m
a
x
(
f
(
m
−
1
,
i
,
j
)
,
f
(
m
,
i
−
m
p
,
j
−
1
)
+
m
)
(
∗
)
f(m,i,j) = max(f(m-1,i,j), f(m,i-m^p,j-1)+m) (*)
f(m,i,j)=max(f(m−1,i,j),f(m,i−mp,j−1)+m)(∗)[不选m号和选多个m号]
公式推导如下:
f
(
m
,
i
,
j
)
=
m
a
x
(
f
(
m
−
1
,
i
,
j
)
,
f
(
m
−
1
,
i
−
m
p
∗
1
,
j
−
1
)
+
m
,
f
(
m
−
1
,
i
−
m
p
∗
2
,
j
−
1
)
+
2
∗
m
,
.
.
.
)
(
1
)
f(m,i,j) = max(f(m-1,i,j), f(m-1,i-m^p*1,j-1)+m, f(m-1,i-m^p*2,j-1)+2*m, ...) (1)
f(m,i,j)=max(f(m−1,i,j),f(m−1,i−mp∗1,j−1)+m,f(m−1,i−mp∗2,j−1)+2∗m,...)(1)
因为是完全背包问题,公式含义如下:[不选m号和选1个m号,选2个m号,选3个m号…]
作变量的等价替换有,
f
(
m
,
i
−
m
p
,
j
−
1
)
=
m
a
x
(
f
(
m
−
1
,
i
−
m
p
,
j
−
1
)
,
f
(
m
−
1
,
i
−
m
p
∗
2
,
j
−
1
)
+
m
,
.
.
.
)
(
2
)
f(m,i-m^p,j-1) = max(f(m-1,i-m^p,j-1), f(m-1,i-m^p*2,j-1)+m, ...) (2)
f(m,i−mp,j−1)=max(f(m−1,i−mp,j−1),f(m−1,i−mp∗2,j−1)+m,...)(2)
因此将(2)式+m,代入(1)式,可以得到(*)式。因此得到了状态转移方程
本题还要求二维背包问题的具体方案,可以倒着推数组,将每一步看成一个决策,总是回溯价值最大的方案
这里求字典序最大的方案,
因此若价值相同应该优先选m号(m号从后往前)而非不选的策略.在回溯过程中输出最大即可
代🐎:
(1) DP
// DP:1107 Integer Factorization (30point(s))
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 410;
const int K = 21; // 20^2就超过400了
int f[K][N][N];
int powr(int a, int b)
{
int re = 1;
for(int i = 1; i <= b; i ++) re *= a;
return re;
}
int main()
{
int n, k, p;
cin >> n >> k >> p;
//初始化数组为负无穷,因为有考虑到没有方案的情况
memset(f, -0x3f, sizeof f);
f[0][0][0] = 0;
// 状态转移方程
int m;
for(m = 1; ; m ++)
{
int v = powr(m, p);
if(v > n) break; // 超过了背包容量
for(int i = 0; i <= n; i ++)
for(int j = 0; j <= k; j ++)
{
f[m][i][j] = f[m-1][i][j]; // 因为i-m^p,j-1可能越界,要单独判断
if(i >= v && j) f[m][i][j] = max(f[m][i][j], f[m][i-v][j-1] + m);
}
}
m --; //注意m++后退出的循环,要减回去
if(f[m][n][k] < 0) puts("Impossible");
else
{
cout << n << " = ";
bool is_first = true;
while(m)
{
int v = powr(m, p);
while(f[m][n-v][k-1] + m >= f[m-1][n][k]) //当价值相同时,总是选m号的决策
{
if(is_first) is_first = false;
else cout << " + ";
cout << m << "^" << p;
n -= v, k -= 1;
}
m --;
}
}
return 0;
}
(2) DFS
// DFS:1107 Integer Factorization (30point(s))
#include <cstdio>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
vector<int> temp, ans, fac; //存放数字
int num, k, p, maxsumN = -1;
//将数字的幂存到fac vector中,下标从1到fac.size()-1 fac[0]=0用不上
void init(){
int i = 1;
fac.push_back(0);
while(true){
int t = pow(i*1.0, p*1.0); //随i增长的幂集
if(t > num) break;
fac.push_back(t);
i++;
}
}
void DFS(int index, int sumK, int sumN, int sumS){//index为fac数组的下标
if(sumK == k && sumS == num){
if(sumN > maxsumN){
maxsumN = sumN;
ans = temp;
}
return;
}
if(sumS > num || sumK > k) return;
//不去招惹下标0.fac[0]
if(index - 1 >= 0){
temp.push_back(index);
DFS(index, sumK + 1, sumN + index + 1, sumS + fac[index]);
temp.pop_back();
DFS(index - 1, sumK, sumN, sumS); //倒叙排列
}
}
int main(){
scanf("%d%d%d", &num, &k, &p);
init();
DFS(fac.size()-1,0,0,0); //DFS入口
int size = ans.size();
//printf("%d",fac.size());
if(maxsumN == -1){
printf("Impossible");
}
else{
printf("%d = %d^%d", num, ans[0], p); //ans[0]存的是下标,下标 + 1 = 实际数字
for(int i = 1; i < size; i++){
printf(" + %d^%d", ans[i], p);
}
}
return 0;
}
二、次重点题目
5.1088 Rational Arithmetic (20分)
思🦌:
这题还是要开long long,做加减通分时做分母的公因子
代🐎:
// 1088 Rational Arithmetic (20分)
#include <iostream>
using namespace std;
typedef long long ll;
ll gcd(ll a, ll b)
{
return b ? gcd(b, a % b) : a;
}
void out(ll a, ll b)
{
bool is_plus = false;
int e = gcd(a, b);
a /= e, b /= e;
if(b < 0) a = -a, b = -b;
int aa = a / b;
int bb = a % b;
if(a < 0) is_plus = true;
if(is_plus) cout << "(";
if(aa < 0 && bb < 0) bb = -bb; // 负号若在带分数的分子上,不表示
if(!a) cout << "0";
if(aa && !bb) cout << aa;
if(aa && bb) cout << aa << " ";
if(bb) cout << bb << "/" << b;
if(is_plus) cout << ")";
return;
}
int main()
{
ll a, b, c, d, e, f;
scanf("%lld/%lld %lld/%lld", &a, &b, &c, &d);
int p = gcd(b, d);
out(a, b), cout << " + ", out(c, d), cout << " = ";
e = d / p * a + b / p * c;
f = b / p * d;
out(e, f), cout << endl;
out(a, b), cout << " - ", out(c, d), cout << " = ";
e = d / p * a - b / p * c;
f = b / p * d;
out(e, f), cout << endl;
out(a, b), cout << " * ", out(c, d), cout << " = ";
e = a * c;
f = b * d;
out(e, f), cout << endl;
out(a, b), cout << " / ", out(c, d), cout << " = ";
if(c == 0) {cout << "Inf" << endl; return 0;}
e = a * d;
f = b * c;
out(e, f), cout << endl;
return 0;
}
5.1096 Consecutive Factors (20point(s))
思🦌:
求连续的最长因子段,思路就是用双指针枚举一下最长的因子段
注意,虽然29=1*29,但它不存在因子段(1不算因子).因此遇到这种情况,ans向量一定是空的,我们最后把n加进去
代🐎:
//1096 Consecutive Factors (20point(s))
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> ans;
for(int i = 2; i <= n / i; i++ )
{
vector<int> tmp;
for(int nn = n, j = i; nn % j == 0; j ++)
{
// 双指针枚举段
tmp.push_back(j);
nn /= j;
}
if(tmp.size() > ans.size()) ans = tmp;
}
if(ans.empty()) ans.push_back(n);
cout << ans.size() << endl;
cout << ans[0];
for(int i = 1; i < ans.size(); i ++)
{
cout << "*" << ans[i];
}
return 0;
}
7.1104 Sum of Number Segments (20point(s))
思🦌:
这题和组合数还不一样,是连续的子段集合。因此枚举左右端点相乘即可。
代🐎:
//1104 Sum of Number Segments (20point(s))
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 100010;
double a[N];
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%lf", &a[i]);
double re = 0;
for(int i = 1; i <= n; i ++)
{
re += a[i] * i * (n - i + 1);
}
printf("%.2lf", re);
return 0;
}
8.1112 Stucked Keyboard (20point(s))
思🦌:
这题调了很久,忘了改变循环指针i要–了,因为下一轮要++回来
代🐎:
//1112 Stucked Keyboard (20point(s))
#include<iostream>
#include<cstring>
using namespace std;
const int N = 200;
int st[N]; // 0:坏的,1:好的,2:输出过的坏的
int main()
{
int k;
string str;
cin >> k >> str;
for(int i = 0; i < str.size(); i ++)
{
int j = i + 1;
while(j < str.size() && str[j] == str[i]) j ++;
int len = j - i;
if(len % k) st[str[i]] = 1; // 好的置1,坏的置0
i = j - 1; // 还要i ++过来,所以i要提前-1
}
string re;
for(int i = 0; i < str.size(); i ++)
{
if(!st[str[i]]) cout << str[i], st[str[i]] = 2;
if(st[str[i]] == 1) re += str[i];
else // 坏的
{
re += str[i];
i += k;
i --; //回退一格
}
}
cout << endl;
cout << re << endl;
return 0;
}
9.1116 Come on! Let’s C (20分)
思🦌:
模拟就完事了
代🐎:
// 1116 Come on! Let's C (20分)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unordered_map>
using namespace std;
const int N = 10010;
int is_list[N];
unordered_map<int, string> M;
bool is_prime(int x)
{
if(x == 1) return false;
for(int i = 2; i <= x / i; i ++)
{
if(x % i == 0) return false;
}
return true;
}
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i ++)
{
int v;
scanf("%d", &v);
if(i == 1) M.insert({v, "Mystery Award"}); //1
else if(is_prime(i)) M.insert({v, "Minion"});
else M.insert({v, "Chocolate"});
is_list[v] = 1;
}
int k;
cin >> k;
while(k --)
{
int m;
scanf("%d", &m);
printf("%04d: ", m);
if(!is_list[m]) printf("Are you kidding?\n");
else{
if(is_list[m] == 1) printf("%s\n", M[m].c_str()), is_list[m] = 2;
else printf("Checked\n");
}
}
return 0;
}
10.1152 Google Recruitment (20分)
思🦌:
这题可以用字符串存,并用c11的stoi(str),substr(begin,len)进行转化
质数我们可以用筛法开一个表,否则的话4e4*1e3=4e7有超时的风险
判断大质数时,因子可以不要是2,4,6,,而是质数表里的数,这样速度会加快许多
代🐎:
//1152 Google Recruitment (20分)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 40010;
bool p[N];
int prime[N];
void init()
{
int id = 0;
memset(p, true, sizeof p);
for(int i = 2; i <= 40000; i ++)
if(p[i]){
prime[id++] = i;
for(int j = i + i; j <= 40000; j += i)
p[j] = false;
}
}
bool check(int x)
{
if(x == 1) return false;
for(int i = 0; prime[i] <= x / prime[i]; i ++)
{
if(x % prime[i] == 0) return false;
}
return true;
}
int main()
{
int l, k;
cin >> l >> k;
string n;
cin >> n;
init();
for(int i = 0; i + k <= n.size(); i ++)
{
int t = stoi(n.substr(i, k));
if(check(t))
{
cout << n.substr(i, k) << endl;
return 0;
}
}
puts("404");
return 0;
}