《信息学奥赛一本通·高手专项训练》集训 Day 3
矩阵快速幂
题目
从前有一种细胞,它的增殖方式很奇怪,经过研究,科学家们找到了一个公式来计算其数量:
由于这个数字可能非常大,科学家们只打算知道 的值。
题解
构造矩阵 ,构造转移矩阵 ,因此 ,用矩阵快速幂计算即可。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
long long read() {
long long x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - 48;
ch = getchar();
}
return x * f;
}
void write(long long x) {
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
const int N = 5;
ll n, x, w;
struct Juz {
long long a[N][N], h, l;
void clean() {
memset(a, 0, sizeof a);
h = l = 0;
}
void build() {
for (int i = 1; i < N; ++i) a[i][i] = 1;
}
} p, q;
long long Fmul(long long a, long long b) {
long long ans = 0;
while (b) {
ans = (ans + a * (b & 1)) % w;
a = (a + a) % w;
b >>= 1;
}
return ans;
}
Juz operator*(const Juz &x, const Juz &y) {
Juz z;
z.clean();
z.h = x.h;
z.l = y.l;
for (int i = 1; i <= x.h; ++i)
for (int j = 1; j <= y.l; ++j)
for (int k = 1; k <= x.l; ++k) z.a[i][j] = (z.a[i][j] + Fmul(x.a[i][k], y.a[k][j]) % w) % w;
return z;
}
Juz Juzqmi(Juz a, long long k) {
Juz ans;
ans.clean();
ans.build();
ans.h = a.h;
ans.l = a.l;
while (k) {
if (k & 1)
ans = ans * a;
a = a * a;
k >>= 1;
}
return ans;
}
int main() {
n = read();
x = read();
w = read();
p.h = 1;
p.l = 3;
p.a[1][1] = 1;
p.a[1][2] = 1;
p.a[1][3] = 1;
q.h = 3;
q.l = 3;
q.a[1][1] = x;
q.a[1][2] = 0;
q.a[1][3] = 0;
q.a[2][1] = 1;
q.a[2][2] = 1;
q.a[2][3] = 0;
q.a[3][1] = 1;
q.a[3][2] = 1;
q.a[3][3] = 1;
p = p * Juzqmi(q, n - 1);
write(p.a[1][1]);
return 0;
}
题目
小 C 数学成绩优异,于是老师给小 C 留了一道非常难的数学作业题:
给定正整数 ,要求计算 的值,其中 是将 所有正整数 顺序连接起来得到的数。
例如, , 。小C 想了大半天终于意识到这是一道不可能手算出来的题目,于是他只好向你求助,希望你能编写一个程序帮他解决这个问题。
题解
设正整数 的位数为 ,则:
对于不同位数的 ,分段使用矩阵快速幂加快递推即可,和第一题类似,先构造矩阵 ,再构造转移矩阵 ,所以:
矩阵快速幂中的指数应为
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll read() {
ll x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - 48;
ch = getchar();
}
return x * f;
}
void write(ll x) {
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
const int N = 10;
ll n, m;
int size(ll x) {
int ans = 0;
while (x) {
ans++;
x /= 10;
}
return ans;
}
ll num(int size) {
ll ans = 1;
for (int i = 1; i <= size; i++) ans *= 10;
return ans;
}
struct Juz {
long long a[N][N], h, l;
inline void clean() {
memset(a, 0, sizeof a);
h = l = 0;
}
inline void build() {
for (int i = 1; i < N; ++i) a[i][i] = 1;
}
};
Juz operator*(const Juz &x, const Juz &y) {
Juz z;
z.clean();
z.h = x.h;
z.l = y.l;
for (int i = 1; i <= x.h; ++i)
for (int j = 1; j <= y.l; ++j)
for (int k = 1; k <= x.l; ++k) z.a[i][j] = (z.a[i][j] + x.a[i][k] * y.a[k][j] % m) % m;
return z;
}
Juz Juzqmi(Juz a, long long k) {
Juz ans;
ans.clean();
ans.build();
ans.h = a.h;
ans.l = a.l;
while (k) {
if (k & 1)
ans = ans * a;
a = a * a;
k >>= 1;
}
return ans;
}
int main() {
n = read();
m = read();
Juz a, b;
a.h = 1;
a.l = 3;
for (int i = 1; i <= 3; i++) a.a[1][i] = 1 % m;
b.h = b.l = 3;
b.a[1][1] = 1 % m;
b.a[1][2] = 0 % m;
b.a[1][3] = 0 % m;
b.a[2][1] = 1 % m;
b.a[2][2] = 1 % m;
b.a[2][3] = 0 % m;
b.a[3][1] = 1 % m;
b.a[3][2] = 1 % m;
b.a[3][3] = 1 % m;
int len = size(n);
for (int i = 1; i < len; i++) {
b.a[1][1] = num(i) % m;
a = a * Juzqmi(b, num(i) - num(i - 1) - (i == 1));
}
b.a[1][1] = num(len) % m;
a = a * Juzqmi(b, n - num(len - 1) + 1 - (len == 1));
cout << a.a[1][1] << endl;
return 0;
}
A 君喜欢收集特殊的字符串,特别是那些能给他带来幸运的串。
A 君认为一个串是幸运串当且仅当这个串包含长度大于 的回文子串。
A 君想知道对于长度恰好为 且字符集大小为 的所有字符串中(字符集中字符不一定都要用上),有多少个不同的幸运串呢?
最后的结果可能很大,A 君只想知道答案对 取模后的值,请你帮助他。
题解
只有当一个字符串不含长度为 或 的回文子串时这个字符串才不是幸运串,可以直接推出答案,答案为字符串总个数减去不是幸运串的字符串的个数,即:
还有一种矩阵快速幂的解法,设 表示长度为 的幸运字符串的个数, 表示长度为 的非幸运串的个数,则有递推式:
仍然构造矩阵 ,转移矩阵为 ,计算要用 ,不然会少 分。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
long long read() {
long long x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - 48;
ch = getchar();
}
return x * f;
}
void write(unsigned long long x) {
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
const int N = 5;
const unsigned ll mod = 1e9 + 7;
unsigned ll n, m;
struct Juz {
unsigned long long a[N][N], h, l;
void clean() {
memset(a, 0, sizeof a);
h = l = 0;
}
void build() {
for (int i = 1; i < N; ++i) a[i][i] = 1;
}
void K_Recursion(int k, int *b) {
clean();
h = l = k;
for (int i = 1; i <= k; i++) a[i][1] = b[i];
for (int i = 1; i < k; i++) a[i][i + 1] = 1;
}
} p, q;
unsigned long long Fmul(unsigned long long a, unsigned long long b) {
unsigned long long ans = 0;
while (b) {
ans = (ans + a * (b & 1)) % mod;
a = (a + a) % mod;
b >>= 1;
}
return ans;
}
Juz operator*(const Juz &x, const Juz &y) {
Juz z;
z.clean();
z.h = x.h;
z.l = y.l;
for (int i = 1; i <= x.h; ++i)
for (int j = 1; j <= y.l; ++j)
for (int k = 1; k <= x.l; ++k) z.a[i][j] = (z.a[i][j] + Fmul(x.a[i][k], y.a[k][j]) % mod) % mod;
return z;
}
Juz Juzqmi(Juz a, unsigned long long k) {
Juz ans;
ans.clean();
ans.build();
ans.h = a.h;
ans.l = a.l;
while (k) {
if (k & 1)
ans = ans * a;
a = a * a;
k >>= 1;
}
return ans;
}
int main() {
n = read();
m = read();
if (n == 1) {
write(0);
return 0;
}
if (n == 2) {
write(m % mod);
return 0;
}
p.h = 1;
p.l = 2;
p.a[1][1] = m % mod;
p.a[1][2] = (Fmul(m, m) % mod - m % mod + mod) % mod;
q.h = 2;
q.l = 2;
q.a[1][1] = m % mod;
q.a[1][2] = 0;
q.a[2][1] = 2;
q.a[2][2] = (m - 2) % mod;
p = p * Juzqmi(q, n - 2);
write(p.a[1][1]);
return 0;
}
质数与约数
题目
在密码系统中,质数扮演了一个很重要的角色。我们对一个称为 的策划很感兴趣,它可以转换密钥。这种方法需要要一个质数 和整数 ,并且 为 的原根。定义:若 为最小的 满足 ,则 为 的原根。
由于我们现在想构建一个这样的系统,所以想找出一个原根的一个列表。你的任务是编写一个程序来帮助我们。我们将给定 以及一系列的 ,你需要判断这些 是否为 的原根。
题解
显然因为 是质数,所以 。
如果学过 ,可以想到直接解方程 求出最小的 ,判断是否 即可。由于 效率太低,所以期望得分 分。
接下来考虑正解:若存在一个数 使得 且 ,那么显然存在一个循环节 ,满足 ,并且 ,(可能是 或 )。
若存在 ,那么有 ,于是问题转化为验证 是否存在。设 为 的一个质因子,若存在 且 ,就有 ,因此离线处理出所有的 ,得到 ,用快速幂计算是否满足所有的 即可。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll read() {
ll x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - 48;
ch = getchar();
}
return x * f;
}
void write(ll x) {
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
const int N = 1e5 + 10;
ll p, n, r, a[N], t;
ll qmi(ll a, ll b) {
ll ans = 1 % p;
while (b) {
if (b & 1)
ans = ans * a % p;
a = a * a % p;
b >>= 1;
}
return ans;
}
int main() {
p = read();
n = read();
ll q = p - 1;
for (ll i = 2; i * i <= q; i++) {
if (q % i == 0) {
a[++t] = i;
while (q % i == 0) {
q /= i;
}
}
}
if (q != 1)
a[++t] = q;
while (n--) {
r = read();
if (r % p == 0) {
puts("NO");
continue;
}
int f = 0;
for (int i = 1; i <= t; i++) {
if (qmi(r, (p - 1) / a[i]) % p == 1) {
f = 1;
break;
}
}
if (f)
puts("NO");
else
puts("YES");
}
return 0;
}
题目
你想将 内的所有自然数分成若干个集合。
一开始每个数自己是一个集合,如果某两个数都是一个 的素数的倍数,则将这两个数所在的集合合并,问最后有多少个集合。
题解
显然可以用素数不断去枚举倍数筛 ,用并查集把筛到的集合合并。
但素数不一定要筛到 ,试想,若存在素数 使得 有产生合并的机会,则 必然至少有两个倍数落在区间 ,即 ,因此 ,只要筛到 即可。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
long long read() {
long long x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - 48;
ch = getchar();
}
return x * f;
}
void write(long long x) {
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
const int N = 2e6 + 10;
ll a, b, p;
ll fa[N], ans;
bool v[N];
void Eratosthenes(ll n) {
memset(v, 0, sizeof(v));
for (ll i = 2; i <= n; i++) {
if (v[i])
continue;
for (ll j = i; i * j <= n; j++) v[i * j] = 1;
}
}
ll get(ll x) { return (fa[x] == x ? x : fa[x] = get(fa[x])); }
int main() {
a = read();
b = read();
p = read();
Eratosthenes(b - a + 1);
for (ll i = a; i <= b; i++) fa[i - a] = i - a;
for (ll i = max(p, (ll)2); i <= b - a + 1; i++) {
if (v[i])
continue;
for (ll j = (a % i ? a / i + 1 : a / i), k = (a % i ? a / i + 1 : a / i); j * i <= b; j++) {
fa[get(j * i - a)] = get(k * i - a);
}
}
for (ll i = a; i <= b; i++)
if (get(i - a) == i - a)
ans++;
write(ans);
return 0;
}
题目
学校宿管有一套神奇的控制系统来控制寝室的灯的开关:

共有 盏灯,标号为 到 ,有 个标有不同质数的开关,开关可以控制所有标号为其标号倍数的灯,按一次开关,所有其控制的灭着的灯都点亮,所有其控制的亮着的灯将熄灭。现在,宿管可以无限的按所有开关,所有灯初始状态为熄灭,请求出最多能点亮几盏灯。
题解
显然最优情况下肯定有方案中每个灯至多按一次,再按就没有意义了。
因此可以想到直接暴力去枚举每个灯按或不按,把灯的亮暗情况用 位 序列来表示,按一次相当于把相关的灯的状态都异或上 ,所以每个开关也可以看成一个 位 操作序列,可以提前预处理,用 可以快速维护。
但 内的质数有大约 个,枚举一定会超时,联想到寿司晚宴这题,我们可以以 为分界线,把小于和大于 的质数分开考虑,小于 的质数最大只有 个,可以暴力枚举他们组合的所有情况;大于 的质数有一个特点,就是选择他只可能被小于 的质数控制的灯,不会影响其他大于 的质数控制的灯,因为 中没有同时拥有两个大于 的质因子的数。
这样,我们就可以在确定小于 的质数开关情况后,一个个考虑大于 的质数的情况,如果一个大于 的质数开关按下后能使亮的灯的个数增多,就贪心地按下它。最后对所有结果取最大值就是答案。
我在代码中为了方便,直接枚举前 小的质数开关的方案,效果是一样的。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
long long read() {
long long x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - 48;
ch = getchar();
}
return x * f;
}
void write(long long x) {
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
const int N = 1010;
int t, n, m, p[N], ans;
bitset<N> e[N];
int main() {
t = read();
while (t--) {
n = read();
m = read();
for (int i = 1; i <= m; i++) {
p[i] = read();
}
ans = 0;
sort(p + 1, p + m + 1);
for (int i = 1; i <= m; i++) {
e[i].reset();
for (int j = 1; j * p[i] <= n; j++) e[i][j * p[i]] = 1;
}
bitset<N> s, t;
for (int i = 0; i < (1 << (min(11, m))); i++) {
s.reset();
for (int j = 0; j < min(11, m); j++) {
if (i & (1 << j))
s = s ^ e[j + 1];
}
int sum = 0;
for (int j = min(11, m) + 1; j <= m; j++) {
t = s ^ e[j];
if (s.count() < t.count())
sum += t.count() - s.count();
}
ans = max(ans, sum + (int)s.count());
}
write(ans);
putchar('\n');
}
return 0;
}

浙公网安备 33010602011771号