文章目录
牛客练习赛129(欧拉筛、快速幂、排列组合、二进制、倍增、线段树、状压DP)
F题不会,看了别人的代码,给大家加个注释看一下吧,
A. 数数(欧拉筛)
题目中,“奇数” = 质数的整次幂。找到所有的质数,维护其小于等于 n 的质数的整次幂的个数即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e6 + 10;
int v[maxn], prime[maxn], cnt = 0;
int f[maxn];
int main(){
int n;
cin >> n;
for(int i = 2; i <= n; i++){
if(v[i] == 0) prime[++cnt] = i;
for(int j = 1; j <= cnt && i * prime[j] <= n; j++){
v[i * prime[j]] = 1;
if(i % prime[j] == 0) break;
}
}
int res = n-1;
for(int i = 1; i <= cnt; i++){
for(long long j = prime[i]; j <= n; j *= prime[i]){
res--;
}
}
cout << res << endl;
return 0;
}
B. 三位出题人(快速幂、排列组合)
根据题意:
-
每个题目可能的出题方案为 m 个人全排列 - 所有人都不选 - 所有人都选,即 2 m − 2 2^m-2 2m−2。( 2 m = ∑ 0 m C ( i , m ) 2^m = \sum_0^mC(i, m) 2m=∑0mC(i,m) )
-
每个题的出题人方案是相互独立的,故而 r e s = ( 2 m − 2 ) n res = (2^m - 2)^n res=(2m−2)n
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll mod = 1e9 + 7;
ll qpow(ll a, ll b){
ll res = 1;
while(b){
if(b&1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
int main(){
int ncase;
cin >> ncase;
while(ncase--){
int n, m;
cin >> n >> m;
ll tmp = (qpow(2, m) - 2 + mod) % mod;
ll res = qpow(tmp, n);
cout << res << endl;
}
return 0;
}
C. 和天下(二进制)
对于k的二进制,按位划分。设 k = 10410 = 11010002:
-
ai 的范围是 0 ~ 1e18,大概就是 263 的样子,考虑 63 个二进制位。
-
设 p = 2x ,且 p > k。满足 ai & p = p 的所有 ai 均可以互连,放入到同一个set中。
-
对于不满足条件2.的情况,划分规则如下图(注意划分到的flag 和 k 二进制比较):
-
对于 k 为偶数时,上述划分方案,不会覆盖到 ai = k的情况,需要多加一个 flag = k
-
对于每一个 ai,如果其属于多个set,把不同set合并。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 2e5 + 10;
ll a[maxn], flag[100];
int f[maxn], sz[maxn];
int find(int x){
if(f[x] == x) return x;
else return f[x] = find(f[x]);
}
void join(int x, int y){
int fx = find(x);
int fy = find(y);
if(fx != fy){
f[fx] = fy;
sz[fy] += sz[fx];
}
}
int main() {
int ncase;
cin >> ncase;
while(ncase--) {
for(int i = 0; i <= 63; i++) flag[i] = -1, f[i] = i, sz[i] = 0;
ll n, k;
cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> a[i];
ll num = 0;
for(int i = 62; i >= 0; i--){ // 划分 flag
ll p = 1ll << i;
if(p > k) flag[i] = p;
else{
num += p;
if(num > k) flag[i] = num;
num -= p;
if((k & p) == p) num += p;
}
}
flag[63] = flag[0] != k ? k : -1; // 处理 flag是否有 k
// 这部分代码是输出每个flag的,可以自己debug一下
// for(int i = 63; i >= 0; i--){
// cout << i << " " << flag[i] << " ";
// if(flag[i] != -1){
// for(int j = 62; j >= 0; j--){
// ll p = 1ll << j;
// if((flag[i] & p) == p) cout << 1;
// else cout << 0;
// }
// }
// cout << endl;
// }
for(int i = 1; i <= n; i++){
for(int j = 0; j <= 63; j++){
// 维护sz, 必须先维护,注意运算符优先级
if(flag[j] != -1 && (a[i] & flag[j]) == flag[j]){
sz[j]++;
break;
}
}
}
for(int i = 1; i <= n; i++){ // 并查集
int u = -1;
for(int j = 0; j <= 63; j++){
if(flag[j] != -1 && (a[i] & flag[j]) == flag[j]){
if(u == -1) u = j;
else join(u, j);
}
}
}
int res = 1;
for(int i = 63; i >= 0; i--){
if(find(i) == i){
res = max(res, sz[i]);
}
}
cout << res << endl;
}
return 0;
}
/*
2
5 104
5 21 1 8 10
*/
D. 搬家(倍增)
设 n e x t ( i ) next(i) next(i)表示,以 i i i 为起点,下一个箱子开始的下标。显然,二分可以快速求出每一个 i 的 n e x t ( i ) next(i) next(i) 。
记 f ( i , j ) f(i,j) f(i,j) 表示以 i i i 为起点, 第 2 j 2^j 2j 个箱子装满后,下一个箱子的下标。
则: f ( i , 0 ) = n e x t ( i ) f(i,0) = next(i) f(i,0)=next(i), f ( i , j ) = f ( f ( i , j − 1 ) , j − 1 ) f(i,j) = f(f(i, j-1), j-1) f(i,j)=f(f(i,j−1),j−1),维护出 f f f 即可。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 2e5 + 10;
ll a[maxn], pre[maxn], f[maxn][30];
int main() {
int ncase;
cin >> ncase;
while(ncase--) {
int n, m, k;
cin >> n >> m >> k;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) pre[i] = pre[i-1] + a[i];
for(int i = 1; i <= n; i++){
int l = i, r = n, pos = -1;
while(l <= r){
int mid = l + r >> 1;
if(pre[mid] - pre[i-1] <= k){
pos = mid;
l = mid + 1;
}
else r = mid - 1;
}
if(pos == -1) f[i][0] = i; // 使用2^i 个箱子后,要装的下一个物品的pos
else f[i][0] = pos+1;
}
for(int j = 1; (1 << j) <= n; j++){ // 倍增
for(int i = 1; i <= n; i++){
if(f[i][j-1] > n) f[i][j] = n+1; // 越过 n 了直接 n+1 即可
else f[i][j] = f[f[i][j-1]][j-1];
}
}
int res = 0;
for(int i = 1; i <= n; i++){
int pos = i, mm = m;
for(int j = 20; j >= 0; j--){
if((1 << j) <= mm){
mm -= (1 << j);
pos = f[pos][j];
}
if(pos == n+1) break; // 已经到了最后一个
}
res = max(res, pos-i);
}
cout << res << endl;
}
return 0;
}
E. Alice and Bod(线段树)
操作一查询任意子串是否对称(区间查询),再考虑操作二是要修改的(区间修改),显然要线段树来解。
操作一:查询子串是否对称,查询 S[l, … ,r] 和 S[r, … , l] 的哈希值是否一致,维护两个线段树,对字符串s 在线段树上维护一个哈希值,一个是字符串s的反转在线段树维护一个哈希值。 (S[r, … , l] 表示字符串S[l, … ,r]的反转)
对于一个位置,维护26个哈希,对应 ‘a’ - ‘z’,如果当前位置字符为 c,则 hash(c ) = 1。可以参考下表:
S | c | a | c | b | a |
---|---|---|---|---|---|
P | P0 | P1 | P2 | P3 | P4 |
‘a’ | 0 * p0 | 0 * p0 + 1 * p1 | 0 * p0 + 1 * p1 + 0 * p2 | 0 * p0 + 1 * p1 + 0 * p2 + 0 * p3 | 0 * p0 + 1 * p1 + 0 * p2 + 0 * p3 + 1 * p4 |
‘b’ | 0 * p0 | 0 * p0 + 0 * p1 | 0 * p0 + 0 * p1 + 0 * p2 | 0 * p0 + 0 * p1 + 0 * p2 + 1 * p3 | 0 * p0 + 0 * p1 + 0 * p2 + 1 * p3 + 0 * p4 |
‘c’ | 1 * p0 | 1 * p0 + 0 * p1 | 1 * p0 + 0 * p1 + 1 * p2 | 1 * p0 + 0 * p1 + 1 * p2 + 0 * p3 | 1 * p0 + 0 * p1 + 1 * p2 + 0 * p3 + 0 * p4 |
操作二:区间加 x,且加x时取mod。对于一个位置,其字符修改后,哈希值整体移动 x 即可。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll base = 2333;
const ll mod = 1e9 + 7;
const int maxn = 1e5 + 10;
typedef struct Node{
ll num[26];
int len = 0, lazy = 0;
Node (){
for (int i = 0; i < 26; i++) {
num[i] = 0;
}
len = lazy = 0;
}
} node;
node tree[2][maxn << 2];
ll p[maxn];
string s[2];
void push_up(int op, int root){
int lroot = root << 1, rroot = root << 1 | 1;
for(int i = 0; i < 26; i++){
tree[op][root].num[i] = tree[op][rroot].num[i] * p[tree[op][lroot].len] % mod;
tree[op][root].num[i] = (tree[op][root].num[i] + tree[op][lroot].num[i]) % mod;
}
tree[op][root].len = tree[op][lroot].len + tree[op][rroot].len;
}
node merge(node A, node B){
node res;
for(int i = 0; i < 26; i++){
res.num[i] = B.num[i] * p[A.len] % mod;
res.num[i] = (res.num[i] + A.num[i]) % mod;
}
res.len = A.len + B.len;
return res;
}
void push_down(int op, int root){
if(tree[op][root].lazy){
int lroot = root << 1, rroot = root << 1 | 1;
tree[op][rroot].lazy = (tree[op][rroot].lazy + tree[op][root].lazy) % 26;
tree[op][lroot].lazy = (tree[op][lroot].lazy + tree[op][root].lazy) % 26;
node tmp = tree[op][lroot];
for(int i = 0; i < 26; i++) tree[op][lroot].num[(i+tree[op][root].lazy) % 26] = tmp.num[i];
tmp = tree[op][rroot];
for(int i = 0; i < 26; i++) tree[op][rroot].num[(i+tree[op][root].lazy) % 26] = tmp.num[i];
tree[op][root].lazy = 0;
}
}
void build(int op, int root, int l, int r){
if(l == r){
tree[op][root].num[s[op][l]-'a'] = 1;
tree[op][root].len = 1;
return;
}
int mid = l + r >> 1;
build(op, root<<1, l, mid);
build(op, root<<1|1, mid+1, r);
push_up(op, root);
}
void update(int op, int root, int l, int r, int L, int R, int x){ // 区间 L,R 加 x
if(L <= l && r <= R){
node tmp = tree[op][root];
for(int i = 0; i < 26; i++){ // 哈希移动
tree[op][root].num[(i+x) % 26] = tmp.num[i];
}
tree[op][root].lazy = (tree[op][root].lazy + x) % 26; // 注意 x 可能很多
return;
}
push_down(op, root);
int mid = l + r >> 1;
if(L <= mid) update(op, root<<1, l, mid, L, R, x);
if(mid < R) update(op, root<<1|1, mid+1, r, L, R, x);
push_up(op, root);
}
node query(int op, int root, int l, int r, int L, int R){ // 查询区间 L,R
// cout << op << " " << root << " " << l << " " << r << " " << L << " " << R << endl;
if(L <= l && r <= R) return tree[op][root];
push_down(op, root);
int mid = l + r >> 1;
node tmp;
if(L <= mid) tmp = merge(tmp, query(op, root<<1, l, mid, L, R));
if(mid < R) tmp = merge(tmp, query(op, root<<1|1, mid+1, r, L, R));
return tmp;
}
int main() {
p[0] = 1;
for(int i = 1; i < maxn; i++) p[i] = p[i-1] * base % mod;
int n, m;
cin >> n >> m;
cin >> s[0];
s[1] = " ";
for(int i = 0; i < n; i++) s[1] = s[1] + s[0][n-1 - i];
s[0] = " " + s[0];
build(0, 1, 1, n);
build(1, 1, 1, n);
while(m--){
int op;
cin >> op;
if(op == 1){
int l, r;
cin >> l >> r;
node t1 = query(0, 1, 1, n, l, r);
node t2 = query(1, 1, 1, n, n+1-r, n+1-l);
int res = 1;
for(int i = 0; i < 26; i++) if(t1.num[i] != t2.num[i]) res = 0;
cout << (res ? "YES" : "NO") << endl;
}
else{
int l, r, x;
cin >> l >> r >> x;
x %= 26;
update(0, 1, 1, n, l, r, x);
update(1, 1, 1, n, n+1-r, n+1-l, x);
}
}
return 0;
}
F. 网络通路(状压DP)
直接看代码的注释。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll e[20][20];
ll dp[(1<<16)][20], f[(1<<16)][20];
ll func(int x){
ll cnt = 0;
while(x){
cnt += (x & 1);
x >>= 1;
}
return cnt;
}
int main() {
memset(e, 0x01, sizeof(e));
memset(dp, 0x01, sizeof(dp));
memset(f, 0x01, sizeof(f));
int n, m;
cin >> n >> m;
ll u, v, c;
for(int i = 1; i <= m; i++){
cin >> u >> v >> c;
u--; v--;
e[u][v] = e[v][u] = min(e[v][u], c); // 重边和自环
}
for(int i = 0; i < n; i++) dp[(1<<i)][i] = 0; // 只有一个点 i 的集合初始化为零
for(int i = 1; i < (1<<n); i++){
for(int j = 0; j < n; j++){ // 维护当前集合,每次
if((1<<j) & i){ // 当前集合i有点 j
int mask = i ^ (1 << j); // 得到集合i去掉点j的子集
for(int p = mask; p; p = (p-1)&mask){ // 枚举子集p 通过点 j 连接子集(i^p),从而维护子集 i 的值
dp[i][j] = min(dp[i][j], dp[i ^ p][j] + f[p][j]);
}
}
}
for(int j = 0; j < n; j++){
if((1<<j) & i){
for(int k = 0; k < n; k++){
if((1<<k) & i) continue;
if(e[j][k] > 1e9) continue;
f[i][k] = min(f[i][k], dp[i][j] + e[j][k] * func(i) * (n - func(i)));
// 通过边 j-k,维护子集 i 通过点 j 向外延申一个点 k 时的贡献
}
}
}
}
ll res = 2e18;
for(int i = 0; i < n; i++) res = min(res, dp[(1<<n)-1][i]);
if(res > 1e9) res = -1;
cout << res << endl;
return 0;
}