这场难度比之前几场友好了许多。
A. Perfectly Imperfect Array
题意: 给出一个数组
a
a
a,判断其是否存在一个子序列
b
b
b使得
b
b
b中的元素乘积不是平方数。若是,输出YES,否则输出NO。
做法: 直接考虑单个元素即可。若
a
a
a中元素有一个不是平方数,输出YES,否则输出NO。证明可以从质因子分解入手(考虑每个质因子次数的奇偶性,代码中判断是否是平方数也用的是质因子分解的方法)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n, a[120], f;
int main()
{
ios::sync_with_stdio(false);
int T;
cin >> T;
while(T--)
{
f = 0;
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
for(int j = 2; j * j <= a[i]; j++)
{
if(a[i] % j) continue;
int cnt = 0;
while(a[i] % j == 0) a[i] /= j, cnt++;
if(cnt & 1) f = 1;
}
if(a[i] != 1) f = 1;
}
cout << (f? "YES" : "NO") << endl;
}
return 0;
}
B. AND 0, Sum Big
题意: 给出 n , k n,k n,k,要求输出满足以下条件的数组的数量,答案模 1 e 9 + 7 1e9+7 1e9+7。
- 数组的元素大小在 [ 0 , 2 k − 1 ] [0,2^{k}-1] [0,2k−1]中。
- 所有元素按位与之后的结果是0。
- 数组元素的和尽量大。
做法: 结合按位与之后的结果是0,说明二进制k位中,每一位都有0,但要是得所有数组的元素和尽量大,那么每一个位有且只有一个0。所以,对于k个可以选择的二进制位来说,每一位选出一个0的方案有n种,因此答案就是 n k n^k nk。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e6 + 10, mod = 1e9 + 7;
ll fac[N], n, k, ans;
ll qpow(ll x, ll n)
{
ll res = 1;
while(n)
{
if(n & 1) res = res * x % mod;
x = x * x % mod;
n >>= 1;
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
int T;
cin >> T;
while(T--)
{
cin >> n >> k;
cout << qpow(n, k) << endl;
}
return 0;
}
C. Product 1 Modulo N
题意: 给出
n
n
n,要求在
[
1
,
2
,
.
.
.
,
n
−
1
]
[1,2,...,n-1]
[1,2,...,n−1]中,选出最长的子序列,使得子序列中所有数的乘积模n的结果为1。
做法: 首先可以明确一点,子序列的元素和
n
n
n的公因子都是1,若不是1,由斐蜀定理,乘积模
n
n
n不可能是1。另外,1一定在任何
n
n
n的答案序列里。赛时打了个表,很容易看出规律。
2: 1
3: 1
4: 1
5: 1 2 3
6: 1
7: 1 2 3 4 5
8: 1 3 5 7
9: 1 2 4 5 7
10: 1 3 7
可以看到,若
i
i
i是答案序列中的元素,那么
n
−
i
n-i
n−i也是,不过有时候
i
i
i可以取1,有的时候不能。我们先将符合条件的
i
i
i加入到答案序列并计算
∏
i
g
c
d
(
i
,
n
)
=
1
a
n
d
2
≤
i
≤
n
−
i
(
n
−
i
)
∗
i
m
o
d
n
\prod \limits_{i}^{gcd(i,n)=1\ and \ 2\le i\le n-i}(n-i)*i \mod n
i∏gcd(i,n)=1 and 2≤i≤n−i(n−i)∗imodn。若结果是1,那么就不加
n
−
1
n-1
n−1到答案序列里,只加入
1
1
1,否则就将
1
1
1和
n
−
1
n-1
n−1加入到答案序列中。
赛时的代码特判了质数,其实不特判也是ok的。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
bool ispr(int x)
{
for(int i = 2; i * i <= x; i++)
{
if(x % i == 0) return false;
}
return true;
}
int main()
{
ios::sync_with_stdio(false);
cin >> n;
if(n <= 4 or n == 6)
{
cout << 1 << endl << 1 << endl;
return 0;
}
if(ispr(n))
{
if(n <= 3) cout << 1 << endl << 1 << endl;
else
{
cout << n - 2 << endl;
for(int i = 1; i <= n - 2; i++)
{
cout << i;
if(i == n - 2) cout << endl;
else cout << ' ';
}
}
}
else
{
vector<int> ans;
ll p = 1;
for(int i = 2; i <= n - i; i++)
{
if(__gcd(i, n) == 1 and __gcd(n, n - i) == 1)
{
p = p * i * (n - i) % n;
ans.push_back(i);
ans.push_back(n - i);
}
}
ans.push_back(1);
if(p != 1) ans.push_back(n - 1);
sort(ans.begin(), ans.end());
cout << ans.size() << endl;
for(int i = 0; i < ans.size(); i++)
{
cout << ans[i];
if(i == ans.size() - 1) cout << endl;
else cout << ' ';
}
}
return 0;
}
D. Cut and Stick
题意: 给出一个长度为
n
n
n的数组
a
a
a和
q
q
q个询问。每次询问给出一个
l
,
r
l,r
l,r,问,将数组区间
[
l
,
r
]
[l,r]
[l,r]切成若干个子序列,使得每个子序列里的数出现次数都不超过
⌈
m
2
⌉
\lceil \frac {m}{2} \rceil
⌈2m⌉,
m
m
m为该子序列的长度,最少需要切分几次。
做法(官方题解): 首先,将
[
l
,
r
]
[l,r]
[l,r]中的元素切分出来,如果序列已经满足要求,那么输出1即可。然后为了顺利A掉题,需要在做题过程中推出下面的结论。我们假设长度为
m
m
m的序列中存在一个出现次数超过半数的元素
s
s
s,其出现频率为
f
f
f,那么最优的切分方法是将剩下的
m
−
f
m-f
m−f元素搭配上
m
−
f
+
1
m-f+1
m−f+1个
s
s
s,其余的
s
s
s切分成单个数的序列。共需要
1
+
f
−
(
m
−
f
+
1
)
=
2
f
−
m
1+f - (m - f + 1)=2f-m
1+f−(m−f+1)=2f−m次。可以证明这样是最优的。
那么就是如何找到这个
f
f
f的问题了。首先,我们得能够高效统计出
a
i
a_i
ai在
[
l
,
r
]
[l,r]
[l,r]内的出现频率。注意到
a
i
a_i
ai的范围不超过
3
e
5
3e5
3e5,可以使用类似权值线段树的思想,用一个数组
v
a
i
v_{a_i}
vai来记录
a
i
a_i
ai在原数组中出现的下标,显然每个
v
a
i
v_{a_i}
vai的元素是单调递增的。那么
a
i
a_i
ai在
[
l
,
r
]
[l,r]
[l,r]内的出现次数可以通过二分查找来实现,
c
n
t
a
i
=
u
p
p
e
r
_
b
o
u
n
d
(
v
a
i
,
r
)
−
l
o
w
e
r
_
b
o
u
n
d
(
v
a
i
,
l
)
cnt_{a_i}=upper\_bound(v_{a_i},r)-lower\_bound(v_{a_i},l)
cntai=upper_bound(vai,r)−lower_bound(vai,l)。
然后是怎么快速查询区间
[
l
,
r
]
[l,r]
[l,r]中,最高的出现次数的问题。
可以使用随机化算法,生成一个处在
[
l
,
r
]
[l,r]
[l,r]内的随机数
d
d
d,然后把
a
[
d
]
a[d]
a[d]当作出现次数最高的元素并统计其出现次数,计算出一个答案
t
m
p
tmp
tmp。重复随机选取
d
d
d的操作30次,将每次的
t
m
p
tmp
tmp与答案取最大值即可(取最大是如果之前的答案比
t
m
p
tmp
tmp小,说明这次选到的数的出现次数比之前的都大,更接近最大出现次数)。
可以建线段树查询。线段树内维护区间
[
l
,
r
]
[l,r]
[l,r]出现次数最大的数(维护的是最大出现次数的话儿子节点合并得到父亲节点的答案会有问题),查询的时候再返回最大的出现次数即可。
这里用到了C++的一个很神奇的区间随机数生成函数,学习一个。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 3e5 + 10;
int a[N], n, q, l, r, t[N << 2];
vector<int> G[N];
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
inline int cnt(int k, int l, int r)
{
return upper_bound(G[k].begin(), G[k].end(), r) - lower_bound(G[k].begin(), G[k].end(), l);
}
int solve_random(int l, int r)
{
int ans = 1;
for(int i = 1; i <= 30; i++)
{
int pos = uniform_int_distribution<int>(l,r)(rng);
ans = max(ans, 2 * cnt(a[pos], l, r) - r + l - 1);
}
return ans;
}
void build(int no, int l, int r)
{
if(l == r)
{
t[no] = a[l];
return;
}
int mid = (l + r) >> 1;
build(2 * no, l, mid);
build(2 * no + 1, mid + 1, r);
t[no] = cnt(t[2 * no], l, r) > cnt(t[2 * no + 1], l, r)? t[2 * no] : t[2 * no + 1];
}
int query(int no, int l, int r, int _l = 1, int _r = 3e5)
{
if(r < l or _r < l or _l > r) return 0;
if(_l >= l and _r <= r) return cnt(t[no], l, r);
int mid = (_l + _r) >> 1;
return max(query(2 * no, l, r, _l, mid), query(2 * no + 1, l, r, mid + 1, _r));
}
int solve_determine(int l, int r)
{
int f = query(1, l, r);
return max(1, 2 * f - (r - l + 1));
}
int main()
{
ios::sync_with_stdio(false);
cin >> n >> q;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
G[a[i]].push_back(i);
}
build(1, 1, 3e5);
while(q--)
{
cin >> l >> r;
cout << solve_random(l, r) << endl;
//cout << solve_determine(l, r) << endl;
}
return 0;
}