T9 超详细题解(最短代码)

首先众所周知这道题目要用 DP 做。

还是比较好好想的:

  1. 定义状态: dp[i] 表示正整数 i 最少需要几个完全平方数所相加。
  2. 初始化:因为一个数 n 最多要用 n1^2 表示,所以将 dp[i] 初始化为 i
  3. 状态转移方程: 对于每个 i(1≤i≤n) ,和每个 j(1≤j^2≤i) 都有 dp[i]=min(dp[i],dp[i-j^2]+1)

好了,这个三要素有了代码就简单了,放个核心代码:

for(int i=1;i<=n;i++)for(int j=1;j*j<=i;j++)dp[i]=min(dp[i],dp[i-j*j]+1);

对,不要惊讶,这道题目很简单,核心 DP 部分只要一行,其他就都是输入输出了。

好了,这篇题解还没有完,因为还有数学的一些拓展:

  • 四平方和定理(拉格朗日四平方和定理)
    • 对于任意正整数 n ,都可以表示为不超过四个正整数的平方和,即 n=a^2+b^2+c^2+d^2 ,其中 a,b,c,d∈N
  • 一种特殊情况: n=4^{a}(8b+7)
    • 根据数论知识可知正整数 n 不能被三个完全平方数和表示的充要条件是 n=4^{a}(8b+7) 其中 a,b∈N 。对于这样的数,我们最少需要用 4 个数来表示
  • 勒让德三平方和定理
    • 一个正整数 n 能表示为三个整数的平方和 n=a^2+b^2+c^2 ,当且仅当 n\not\equiv 7 (\mathrm{mod}\ 8)n\not\equiv 4^{k}(8m+7),\ k,m∈N 。这意味着如果一个数不满足 n=4^k(8m+7) 这种形式,那么它就可以用三个完全平方数的和来表示。
  • 费马平方和定理
    • 奇质数能表示为两个平方数之和 p=x^2+y^2, \ x,y∈Z 的充要条件是 p\equiv 1(\mathrm{mod} \ 4) 。也就是说,当奇质数除以 4 的余数为 1 时,它可以表示为两个完全平方数之和,反之也成立。
    • 对于一般的正整数 n ,能表示为两个完全平方数之和的充分必要条件是: n 的所有形如 4k+3, \ k∈Z 的质因数的幂次都是偶数。
    • 证明的思路:
      • 必要性:假设 p 是一个能表示为两个平方数之和的奇质数,即 p=x^2+y^2 。通过对 xy 的奇偶性分析以及模运算的性质,可以推出。
      • 充分性:对于的奇质数 p ,利用数论中的一些工具,如二次剩余理论、高斯整数环的性质等,可以构造出满足 p=x^2+y^2 的整数 xy 。对于一般正整数 n ,根据其质因数分解以及唯一分解定理,结合奇质数的情况来证明其能表示为两个完全平方数之和的条件。
  • 那么如果一个数最少被一个完全平方数表示的条件是什么?当然是这个数自己是完全平方数啦,一定要说出充要条件的话就是对于一个数 n∈N 它的算术平方根为整数时成立,即 \sqrt{n} ∈ Z ,想用 C++ 完成判断可以用 Miller_rabin 素性检测来实现。

根据我的习惯讲到判断素数就必须要将!

  1. 首先我们有第一种方法试除法:
    这肯定就是枚举啦,但是我们可以优化哦!
    比如当你枚举到 \sqrt n 的时候你会发现后面在枚举就会出现重复所以我们枚举到 \sqrt n 就行了!时间复杂度为 O(\sqrt n) 好了这个太简单了,放个代码:
#include<bits/stdc++.h>
#define I using
#define AK namespace
#define IOI std
#define i_ak return
#define ioi  0
I AK IOI;
int n;
int main(){
	//freopen("","r",stdin);
	//freopen("","w",stdout);
	cin>>n;
	for(int i=2;i*i<=n;i++){
		if(n%i==0){
			puts("no");
			return 0;
		}
	}
	puts("yes");
	i_ak ioi;
}
  1. 然后就是埃式筛与欧拉筛:
    其实这两种筛法都围绕了同一个思想就是: 素数的倍数不是素数。
    埃式筛的思想就是给出从 2n 的所有数,并且从 2 开始将 2 的倍数并且不大于 n 的数全部去掉,以此类推,直到到 \sqrt n 结束。没有被去掉的数,就是素数。
    这种筛法的时间复杂度为: O(n\log \log n)
    放个模板代码:
#include<bits/stdc++.h>
#define I using
#define AK namespace
#define IOI std
#define i_ak return
#define ioi  0
I AK IOI;
int n,i,j,flag,a[100005];
int main(){
	cin>>n;
    for(i=2;i<=n/i;i++)if(a[i]==0)for(j=2;i*j<=n;j++)a[i*j]=1;
    for(i=2;i<=n;i++){
        if(a[i]==0){
            flag=1;
            cout<<i<<endl;
        }
    }
    i_ak ioi;
}

而欧拉筛的时间复杂度为 O(n)
然后我们放一个欧拉筛的模板代码:

#include<bits/stdc++.h>
#include<bitset>//需要引入头文件
#define I using
#define AK namespace
#define IOI std
#define i_ak return
#define ioi  0
I AK IOI;
bitset<1000001>a;//定义方式,这里就相当于定义bool a[1000001];
int b[100001];//存质数 
long long n,cnt;
int main(){
    cin>>n;
    for (int i=2;i<=n;i++){
        if(!a[i])b[++cnt]=i;
        for(int j=1;j<=cnt;j++){
            if(i*b[j]>n)break;//如果超出给出的范围,那么就退出循环 
            a[i*b[j]]=1;//素数的倍数不是素数,进行标记。
            if(i%b[j]==0)break;//超级关键的只标记一次
        }
    }
    for(int i=1;i<=cnt;i++)cout<<b[i]<<' ';
}

注意这里我们使用了 bitset 是因为当 n 的范围过大时,其他像 vector 占用的内存会很高,而这里的 bitsetbool 类型差不多也是每一个位置存储 01 并且它每一个位置只需要占用 1\ \ bit 的空间十分划算。
记下来我们来讨论一个问题:为什么欧拉筛更快?
因为埃式筛它会重复标记一个数,比如: 6 它在 2 被标记过一次,在 3 也标记过一次这样就会浪费很多时间,那欧拉筛呢?
我们注意到 if(i%b[j]==0)break; 这一个判断让我们不会重复计算同一个数,这样就不会浪费时间了!

  1. 接下来我们来讲 Miller_rabin 素性检测!
    算法原理我们是依据了费马小定理:若 p 为质数,则 a^{p-1}\equiv 1 (\mathrm{mod} \ p) 但是我们是知道费马小定理的逆命题是不成立的,比如:卡迈克尔数。
    但是我们可以把这个错误率降到最小,算法还是很好的。
    接着我们还需要知道二次探测定理
    p∈\mathbb{P} ,a^2 \equiv 1(\mathrm{mod} \ P) 那么 a\equiv ± 1(\mathrm{mod} \ P)
    有定理不能不证明!
    首先我们移项得到: \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ a^2-1 \equiv 0(\mathrm{mod} \ P)\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (a+1)\times(a-1)\equiv 0(\mathrm{mod} \ P)
    那么我们有要么是 (a+1)\equiv 0(\mathrm{mod} \ P) 要么是 (a-1)\equiv 0(\mathrm{mod} \ P)
    a \equiv ±1(\mathrm{mod} \ P)
    现在证明完毕了,我们假设要判断的素数为 p 那么将 p-1 分解为 2^k\times t 的形式。
    那么当 p 为素数时,有 a^{2^k\times t}\equiv 1(mod \ p)
    我们随机选择一个数 n 算出 a^t (\mathrm{mod} \ p) 的值,让其不断地自乘,结合二次探测定理进行判断,如果我们乘出来的数模 p1 ,但是我们之前的数并不符合,那么这个数就是合数。
    这样一直乘 k 次,最后得到的数就是 a^{p-1}
    那么如果最后计算出来的数不是 1 那么这个数也是合数!
    那么算法复杂度是多少呢?非常乐观只有 O(k \log n) 其中 k 未检测次数。
    放上模板代码(只放函数部分):
int primes[19]={2,3,5,7,11,13,17,19,23,29,31,37};
ll poww(ll a,ll b,ll c){
    ll ans=1;__int128 base=a;
    while(b){
        if(b&1)ans=ans*base%c;
        base=base*base%c;
        b>>=1;
    }
    return ans;
}
bool miller_rabin(ll n){
    for(int i=0;i<=8;i++){
        if(primes[i]==n)return 1;
        else if(primes[i]>n)return 0;
        ll t=poww(primes[i],n-1,n),x=n-1;
        if(t!=1)return 0;
        while(t==1&&x%2==0){
            x/=2;
            t=poww(primes[i],x,n);
            if(t!=1&&t!=n-1)return 0;
        }
    }
    return 1;
}

那么由 Miller_rabin 算法衍生的算法还有很多,比如说要用到素数判断的 pfc 和 Pollard_rho 。

好了题解就到这里了,我们讲解了 DP 思路和数学拓展,希望对您有帮助哦~

打字不易,留下您的一些小心意吧~

13 个赞

666原来是数学家

2 个赞

@金裕仓 只是一些爱好,不过用数学做C++题目真的很爽

3 个赞

@金裕仓 呜呜呜,没有人点赞吗?是写得还不够好吗?那我再补充一下

3 个赞

挺好的,加油

1 个赞

@徐千城俊 我要准备补充题解了

3 个赞

行,

2 个赞

更新完毕

3 个赞
#include<bits/stdc++.h>
#define I using
#define AK namespace
#define IOI std
#define i_ak return
#define ioi  0
I AK IOI;
int n;
int main(){
	//freopen("","r",stdin);
	//freopen("","w",stdout);
	cin>>n;
	for(int i=2;i*i<=n;i++){
		if(n%i==0){
			puts("no");
			return 0;
		}
	}
	puts("yes");
	i_ak ioi;
}

为啥要这么写啊(是凑代码的吗

2 个赞

@徐千城俊 啥意思,你是哪里不懂?

3 个赞

这前面一堆…

2 个赞

@徐千城俊 这是我的缺省源!

3 个赞

o,是这样

2 个赞

借此机会宣个
(因为不想搞太复杂,所以写了 n\le10^{12} ,其实 n\le10^{18} 也是可以做的)

1 个赞

@张乐凡 哎呀这道题目已经够多了,都有原题,谁都知道怎么做了,一堆原题

1 个赞

力扣,GESP,洛谷
算是一道经典背包问题了

1 个赞

你这个样例有问题吧

1 个赞

string dp[100005] 呢???被你吃了!?!

1 个赞

输入格式

本题含有多组测试数据
第一行一个整数 T ,代表数据组数。
接下来 T 行,每行一个整数 n

输出格式

对于每组测试数据,输出 n 最少可以表示为多少个正整数的平方和。

样例 #1

样例输入 #1

2
36
7
125

样例输出 #1

1
4
2

第一行一个整数 T ,代表数据组数。
接下来 T 行,每行一个整数 n

样例输入是应该是:

3
36
7
125
1 个赞

我提交了,你猜怎么着:

UKE

1 个赞