1130考试总结
T1
题目大意:
给定(n)个数, 将这(n)个数字放入两个集合(S,T)中, 不能为空集, 求(gcd(prod_{i in S} a_i, prod_{i in T} a_i) = 1)的方案数, 答案对(10^9+ 7)取模.(n <= 10^5, a_i <= 10^6).
数学 + 并查集.
假设没有题目的限制, 把(n)个数放入两个集合并且没有空集的方案数是 : (2^n - 2 = C_n^1 + C_n^2 + ... + C_n^{n - 1})
加入了这个限制怎么办呢? 我们可以发现, 两个数有相同质因子的一定是要放在同一个集合里面的. 所以我们对具有相同质因子的数字连边, 最后求出联通块的个数(cnt), 那么答案就是 : (2^{cnt} - 2).
其实可以用并查集来维护连通性, 可以降低复杂度.
#include <bits/stdc++.h>
using namespace std;
inline long long read() {
long long s = 0, f = 1; char ch;
while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
return s * f;
}
const int N = 1e5 + 5, M = 1e6 + 5, mod = 1e9 + 7;
int n, tag, cnt, ans;
int p[M], fa[M], tmp[M], prime[N], is_prime[M];
void make_pre() {
for(int i = 2;i < M; i++) {
if(!is_prime[i]) { prime[++ cnt] = i; p[i] = i; }
for(int j = 1;j <= cnt && i * prime[j] < M; j++) {
is_prime[i * prime[j]] = 1;
p[i * prime[j]] = prime[j];
if(!(i % prime[j])) break;
}
}
}
int ksm(int x, int y) {
int res = 1; while(y) { if(y & 1) res = 1ll * res * x % mod; x = 1ll * x * x % mod; y >>= 1; } return res;
}
int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
int main() {
make_pre();
for(int T = read(); T ; T --) {
cin >> n;
ans = cnt = 0;
for(register int i = 1;i <= M - 5; i++) fa[i] = i;
for(register int i = 1, x;i <= n; i++) {
x = read();
if(x == 1) { ans ++; continue ; }
int las = 0;
while(x >= 2) {
int xx = p[x]; tmp[++ cnt] = xx;
while(!(x % xx)) x /= xx;
if(las) {
int fx = find(las), fy = find(xx);
if(fx != fy) fa[fx] = fy;
}
las = xx;
}
}
sort(tmp + 1, tmp + cnt + 1);
cnt = unique(tmp + 1, tmp + cnt + 1) - tmp - 1;
for(register int i = 1;i <= cnt; i++) if(fa[tmp[i]] == tmp[i]) ans ++;
printf("%d
", ksm(2, ans) - 2);
}
return 0;
}
T2
题目大意 :
给定(n)个节点(m)条边的无向图, 每条边都有一个权值(c = {0, 1}), 现在让你找一条长度为(d)的路径, 问不同的路径有多少, 两条路径不同是指着两条路径任意位置的权值不同, 也就是两个不同的01串.
(n <= 90, m <= n*(n- 1), d <= 30).
meet in middle + 状压DP.
首先这个(d)太大了, 肯定不能直接搜, 我们考虑把一条路径劈一半, 用两条长度为(frac{d}{2})的路径组合起来, 这是meet in middle.
怎么找路径呢? $ dp[i][j]$ 表示从节点(u)出发((u)是我们枚举的), 是否存在一条状态为(i)的边到节点(j).(f[i][j])表示从(j)走是否存在一条状态为(i)的路径, 不管到那个节点.
我们可以发现, 如果(dp[i][j])和(f[i][j])都为1的话, 那么就存在一条合法路径, 中转点是(j), 但此时起点还不是1;
我们从(n)到1枚举(u), 先不管怎么转移, 反正到枚举完(u)之后(dp)数组的含义就变为从1开始了, 然后按照我们上面说的找长度为(d)的路径.
设(g0[i])是从(i)可以经过一条边权为0的边到的节点,, (g1[i])是从(i)可以经过一条边权为1的边到的节点 是一个二进制数, 可以用(bitset)存. 如果当前(dp[s][x] = 1), 说明存在一条从(u)到(x)的路径, 路径的状态为(s).假设下一条边走的是边权为0的边 : (dp[s << 1] |= g0[x]), 说明存在一条状态为(s << 1)的路径从(u)到其他节点.那么走边权为1的节点也同理 : (dp[s << 1 | 1] |= g1[x]).
转移(f)就更简单了, (f[s][u] = dp[1 << d1 | s].any()), (.any())是(bitset)的函数, 看一个二进制数里是否有1.
但是还有一个问题, 对于以这种状态的路径我们无法区分 : (101, 0101), 前者是长度为3的路径, 后者是长度为4的路径, 但是他们的二进制是一样的, 这就需要我们在他们前面补个1, 变成了:(1101, 10101), 这样就可以区分开了, 具体可以对照代码理解.
#include <bits/stdc++.h>
using namespace std;
inline long long read() {
long long s = 0,f = 1; char ch;
while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
return s * f;
}
const int N = 100, M = (1 << 20) + 1;
int n, m, d, ans;
bitset<N> g0[N], g1[N], f[M], dp[M];
int main() {
n = read(); m = read(); d = read();
for(int i = 1, x, y, w;i <= m; i++) {
x = read(); y = read(); w = read();
if(w) g1[x][y] = g1[y][x] = 1;
else g0[x][y] = g0[y][x] = 1;
}
int d2 = d / 2, d1 = d - d2;
for(int u = n; u >= 1; u --) { //dp[i][j]表示从u出发是否存在一条状态为i的路径到j
for(int s = 0;s < M; s ++) dp[s].reset();
dp[1][u] = 1;
for(int s = 1;s < (1 << d1); s ++)
for(int x = 1;x <= n; x ++)
if(dp[s][x]) {
dp[s << 1] |= g0[x]; dp[s << 1 | 1] |= g1[x];
}
for(int s = 0;s < (1 << d1); s ++) { //f[i][j]表示从j出发是否有一条状态为i的路径
f[s][u] = dp[1 << d1 | s].any();
}
}
for(int i = 0;i < (1 << d1); i++)
for(int j = 0;j < (1 << d2); j++) if((dp[1 << d2 | j] & f[i]).any()) ans ++; //枚举中间节点
printf("%d", ans);
return 0;
}
T4
题目链接
我们可以发现, 最终的合法序列一定是一个单峰的.
先说一下我考场上的错误思路 : 求出(f[i], g[i]), 然后枚举合并, (f[i])是从1到(i)变成单调不降序列的次数, (g[i])是从(i)到(n)变成单调不升序列的次数.
但是这么做是错误的, 比如这个样例 : 7 9 3 9 6 4 1 4 10 9;
正确的答案最后应该是变成了 : 3 7 9 9 9 10 6 4 4 1, 而上面做法只能变成这样 : 3 7 9 9 10 9 6 4 4 1, 因为上面做法最后的9不可能会跑到10前面去.
那怎么办呢? 我们可以知道, 当前数字变到正确的位置, 他肯定要与一些数字交换, 假设峰在当前数字的右侧, 那么它左侧比他大的数字一定会与它交换一次, 假设峰在当前数字的左侧, 那么它右侧侧比他大的数字一定会与它交换一次. 所以我们统计一个数字左边,右边比它大的数字的个数, 然后取较小的那个加到答案里就好了.
#include <bits/stdc++.h>
#define int long long
using namespace std;
inline long long read() {
long long s = 0, f = 1; char ch;
while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
return s * f;
}
const int N = 3e6 + 5;
int n, a[N], b[N], c[N], t[N];
long long ans, f[N], g[N];
int lowbit(int x) { return x & -x; }
void change(int x) { for( ; x < N ; x += lowbit(x)) t[x] ++; }
long long query(int x) { long long res = 0; for( ; x ; x -= lowbit(x)) res += t[x]; return res; }
signed main() {
n = read();
for(int i = 1;i <= n; i++) a[i] = b[i] = read();
sort(b + 1, b + n + 1);
int cnt = unique(b + 1, b + n + 1) - b - 1;
for(int i = 1;i <= n; i++) a[i] = lower_bound(b + 1, b + cnt + 1, a[i]) - b, c[n - i + 1] = a[i];
for(int i = 1;i <= n; i++) {
f[i] = i - 1 - query(a[i]); change(a[i]);
}
memset(t, 0, sizeof(t)); ans = 0;
for(int i = 1;i <= n; i++) {
g[i] = i - 1 - query(c[i]); change(c[i]);
}
for(int i = 1;i <= n; i++) ans += min(f[i], g[n - i + 1]);
printf("%lld
", ans);
return 0;
}