2019牛客暑期多校训练营(第二场)
Contest Info
[Practice Link](https://ac.nowcoder.com/acm/contest/882#question)
Solved | A | B | C | D | E | F | G | H | I | J |
---|---|---|---|---|---|---|---|---|---|---|
7/10 | Ø | Ø | - | Ø | Ø | O | - | O | - | Ø |
- O 在比赛中通过
- Ø 赛后通过
- ! 尝试了但是失败了
- - 没有尝试
Solutions
A.Eddy Walker
题意:
在一个长度为\(n\)的环上,环上的点标号为\([0, n - 1]\),问从\(0\)出发,每次等概率的往左走或者往右走,直到所有点都经过那么该过程就停止,问最后停在\(m\)点的概率是多少?
有\(T\)组数据,输出前\(i\)组数据都发生的概率
思路:
- 可以小数据打表一下看看概率大概在多少,其实可以发现是\(\displaystyle \frac{1}{n - 1}\)
- 当\(n = 1, m = 0\)的时候概率是\(1\)
- 可以考虑环上某个点\(M\),如果以这个点终止的话,那么表示上一步肯定是从左边或者右边转移过来的。
- 然后发现这是一个环,实际上每个点都是这种状况,所以除了\(0\)这个点以外,其它点的概率应该是相同的。
- 所以概率均分一下就是\(\displaystyle \frac{1}{n - 1}\)
- 但是要输出前\(i\)组数据都发生的概率,那么求一个前缀积即可
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll p = (ll)1e9 + 7;
ll qmod(ll base, ll n) {
ll res = 1;
while (n) {
if (n & 1) res = res * base % p;
base = base * base % p;
n >>= 1;
}
return res;
}
int n, m;
int main() {
ll res = 1;
int T;
scanf("%d", &T);
while(T--) {
scanf("%d %d", &n, &m);
if (n == 1) {
res *= 1;
} else if (m == 0) {
res = 0;
} else {
res = res * qmod(n - 1, p - 2) % p;
}
printf("%lld\n", res);
}
return 0;
}
B.Eddy Walker 2
题意:
在一个无限长的直线上,从\(0\)出发,每次可以往下走\(1\)步、\(2\)步、\(\cdots\)、\(k\)步,问最后恰好落在\(m\)点的概率。
思路:
令\(f[x]\)表示恰好落在第\(x\)点的概率,\(S[x]\)表示前\(x\)的点的概率和。
那么有转移:
- \(f[x] = \frac{1}{k} (S[x - 1] - S[x - k - 1])\)
但是\(n\)很大,\(k\)也很大,矩阵快速幂都快速不了。。
那其实这就是线性递推,上个BM就可以了。
但其实最大的问题是\(n = -1\)的时候即\(n \rightarrow \infty\)的时候。
其实还是可以小数据打表一下。
发现答案大概是\(\frac{2}{k + 1}\)
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define N 1000010
const ll p = 1e9 + 7;
ll n, m, k, inv2, invk;
ll f[N], g[N], fac[N], inv[N];
ll qmod(ll base, ll n) {
base %= p;
ll res = 1;
while (n) {
if (n & 1) {
res = res * base % p;
}
base = base * base % p;
n >>= 1;
}
return res;
}
void add(ll &x, ll y) {
x += y;
if (x >= p) x -= p;
}
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)
#define pb push_back
#define mp make_pair
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef vector<int> VI;
typedef pair<int, int> PII;
const ll mod = 1000000007;
ll powmod(ll a, ll b) { ll res = 1; a %= mod; assert(b >= 0); for (; b; b >>= 1) { if (b & 1)res = res * a%mod; a = a * a%mod; }return res; }
// head
int _;
namespace linear_seq {
ll res[N], base[N], _c[N], _md[N];
vector<int> Md;
void mul(ll *a, ll *b, int k) {
rep(i, 0, k + k) _c[i] = 0;
rep(i, 0, k) if (a[i]) rep(j, 0, k) _c[i + j] = (_c[i + j] + a[i] * b[j]) % mod;
for (int i = k + k - 1; i >= k; i--) if (_c[i])
rep(j, 0, SZ(Md)) _c[i - k + Md[j]] = (_c[i - k + Md[j]] - _c[i] * _md[Md[j]]) % mod;
rep(i, 0, k) a[i] = _c[i];
}
int solve(ll n, VI a, VI b) { // a 系数 b 初值 b[n+1]=a[0]*b[n]+...
// printf("%d\n",SZ(b));
ll ans = 0, pnt = 0;
int k = SZ(a);
assert(SZ(a) == SZ(b));
rep(i, 0, k) _md[k - 1 - i] = -a[i]; _md[k] = 1;
Md.clear();
rep(i, 0, k) if (_md[i] != 0) Md.push_back(i);
rep(i, 0, k) res[i] = base[i] = 0;
res[0] = 1;
while ((1ll << pnt) <= n) pnt++;
for (int p = pnt; p >= 0; p--) {
mul(res, res, k);
if ((n >> p) & 1) {
for (int i = k - 1; i >= 0; i--) res[i + 1] = res[i]; res[0] = 0;
rep(j, 0, SZ(Md)) res[Md[j]] = (res[Md[j]] - res[k] * _md[Md[j]]) % mod;
}
}
rep(i, 0, k) ans = (ans + res[i] * b[i]) % mod;
if (ans<0) ans += mod;
return ans;
}
VI BM(VI s) {
VI C(1, 1), B(1, 1);
int L = 0, m = 1, b = 1;
rep(n, 0, SZ(s)) {
ll d = 0;
rep(i, 0, L + 1) d = (d + (ll)C[i] * s[n - i]) % mod;
if (d == 0) ++m;
else if (2 * L <= n) {
VI T = C;
ll c = mod - d * powmod(b, mod - 2) % mod;
while (SZ(C)<SZ(B) + m) C.pb(0);
rep(i, 0, SZ(B)) C[i + m] = (C[i + m] + c * B[i]) % mod;
L = n + 1 - L; B = T; b = d; m = 1;
}
else {
ll c = mod - d * powmod(b, mod - 2) % mod;
while (SZ(C)<SZ(B) + m) C.pb(0);
rep(i, 0, SZ(B)) C[i + m] = (C[i + m] + c * B[i]) % mod;
++m;
}
}
return C;
}
int gao(VI a, ll n) {
VI c = BM(a);
c.erase(c.begin());
rep(i, 0, SZ(c)) c[i] = (mod - c[i]) % mod;
return solve(n, c, VI(a.begin(), a.begin() + SZ(c)));
}
};
int main() {
m = 1000000;
fac[0] = 1;
for (int i = 1; i < N; ++i) fac[i] = 1ll * fac[i - 1] * i % p;
inv[m] = qmod(fac[m], p - 2);
for (int i = m; i >= 1; --i) inv[i - 1] = inv[i] * i % p;
inv2 = 5e8 + 4;
int T; scanf("%d", &T);
while (T--) {
scanf("%lld%lld", &k, &n);
invk = qmod(k, p - 2);
if (n == -1) {
printf("%lld\n", 2 * qmod(k + 1, p - 2) % p);
continue;
}
for (int i = 1; i <= m; ++i) f[i] = 0;
f[0] = 1; g[0] = 1;
for (int i = 1; i <= m; ++i) {
if (i > k) {
add(f[i], invk * (g[i - 1] - g[i - k - 1] + p) % p);
} else {
add(f[i], invk * g[i - 1] % p);
}
g[i] = (g[i - 1] + f[i]) % p;
}
vector <int> vec;
for (int i = 0; i <= 2 * k; ++i) vec.push_back(f[i]);
printf("%d\n", linear_seq::gao(vec, n));
}
return 0;
}
D.Kth Minimum Clique
题意:
给出\(n\)个点,求第\(k\)小团的权值和。
思路:
考虑爆搜,用小根堆维护权值,每次取出一个团,考虑加入一个点,判断加入的点是否使得子图依然是一个团可以用bitset优化。
然后出队\(k\)次即可。
每次加入一个点的时候只考虑已有的编号最大的点的右边的点,可以防止重复。
时间复杂度\(\mathcal{O}(klogk + kn^2/64)\)
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define N 110
#define B bitset <110>
int n, k, a[N];
B G[N];
struct node {
ll val;
B mask;
node() {}
node(ll val, B mask) {
this->val = val;
this->mask = mask;
}
bool operator < (const node &other) const {
return val > other.val;
}
};
int main() {
while (scanf("%d%d", &n, &k) != EOF) {
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
for (int i = 1; i <= n; ++i) {
G[i].reset();
for (int j = 1, x; j <= n; ++j) {
scanf("%1d", &x);
G[i][j] = x;
}
}
priority_queue <node> pq;
B tmp; tmp.reset();
pq.push(node(0, tmp));
while (!pq.empty()) {
node top = pq.top(); pq.pop();
if (--k == 0) {
printf("%lld\n", top.val);
return 0;
}
int pos = 1;
for (int i = 1; i <= n; ++i) {
if (top.mask[i]) {
pos = i + 1;
}
}
for (int i = pos; i <= n; ++i) {
if ((G[i] & top.mask) == top.mask) {
top.mask[i] = 1;
pq.push(node(top.val + a[i], top.mask));
top.mask[i] = 0;
}
}
}
puts("-1");
}
return 0;
}
E.Maze
题意:
有一个\(n \cdot m\)的地图,\(b_{i, j} = 1\),表示那里是墙,否则可以走。
现在要求询问从\(b_{1, a}\)走到\(b_{n, b}\)的方案数。
思路:
我们考虑用\(dp[i][j]\)表示从\((i - 1, j)\)走到\((i, j)\)的方案数。
那么就有如下转移:
那么写成矩阵形式:
其中\(A_{i, j}\)表示\((i - j)\)这一段是否全是\(0\)
然后有翻转操作,可以用线段树维护乘法过程。
然后对于询问操作,我们令\(dp[1][a] = 1\),答案就是\(dp[n + 1][n]\),因为我们定义的状态是从\((i - 1)\)层转移过来的,那么显然可以从第\(n\)层走过来,满的状态就是\(dp[n + 1][b]\)
然后对于最终结果是\(A[a][b]\)还是\(A[b][a]\)的问题,我发现其实是跟矩阵的左乘还是右乘有关。而跟如何摆放\(A_{i, j}\)无关,因为它好像是对称的?
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define N 50010
const ll p = 1e9 + 7;
int n, q, m;
int a[N][12];
template <class T1, class T2>
void add(T1 &x, T2 y) {
x += y;
if (x >= p) x -= p;
}
struct SEG {
struct node {
int a[11][11];
node() {
memset(a, 0, sizeof a);
}
void modify(int *b) {
memset(a, 0, sizeof a);
for (int j = 1; j <= m; ++j) {
for (int i = j; i <= m && b[i] == 0; ++i) {
a[i][j] = 1;
}
for (int i = j; i >= 1 && b[i] == 0; --i) {
a[i][j] = 1;
}
}
}
node operator * (const node &other) const {
node res = node();
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= m; ++j) {
for (int k = 1; k <= m; ++k) {
add(res.a[i][j], 1ll * a[i][k] * other.a[k][j] % p);
}
}
}
return res;
}
}t[N << 2];
void build(int id, int l, int r) {
if (l == r) {
t[id].modify(a[l]);
return;
}
int mid = (l + r) >> 1;
build(id << 1, l, mid);
build(id << 1 | 1, mid + 1, r);
t[id] = t[id << 1] * t[id << 1 | 1];
}
void update(int id, int l, int r, int pos) {
if (l == r) {
t[id].modify(a[l]);
return;
}
int mid = (l + r) >> 1;
if (pos <= mid) update(id << 1, l, mid, pos);
else update(id << 1 | 1, mid + 1, r, pos);
t[id] = t[id << 1] * t[id << 1 | 1];
}
}seg;
int main() {
while (scanf("%d%d%d", &n, &m, &q) != EOF) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
scanf("%1d", a[i] + j);
}
}
seg.build(1, 1, n);
int op, x, y;
while (q--) {
scanf("%d%d%d", &op, &x, &y);
if (op == 1) {
a[x][y] ^= 1;
seg.update(1, 1, n, x);
} else {
printf("%d\n", seg.t[1].a[x][y]);
}
}
}
return 0;
}
F.Partition problem
题意:
给出\(2n\)个人,要将这\(2n\)个人划分成两组\(A\)和\(B\),使得每组都有\(n\)个人,那么贡献就是\(\sum\limits_{i \in A, j \in B} v[i][j]\),问怎么分组使得贡献最大。
思路:
考虑爆搜,再考虑几条剪枝:
- 算贡献的时候边加点边算
- 如果某一边已经满了,那么剩下的全都给另一边即可
- 把出口判断放在入口处
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 40
int n, m;
int v[N][N];
ll ans, tnow;
int arr[N], brr[N];
inline void calc() {
ll res = 0;
for (register int i = 1; i <= m; ++i) {
for (register int j = 1; j <= m; ++j) {
res += v[arr[i]][brr[j]];
}
}
ans = max(ans, res);
}
void DFS(int pos, int cnt, ll now) {
// 1
if (cnt == n - pos + 1) {
int tmp = arr[0];
tnow = now;
for (register int i = pos; i <= n; ++i) {
arr[++arr[0]] = i;
for (int j = 1; j <= m; ++j) {
tnow += v[i][brr[j]];
}
}
ans = max(ans, tnow);
arr[0] = tmp;
return;
}
if (cnt) {
arr[++arr[0]] = pos;
tnow = now;
for (int j = 1; j <= brr[0]; ++j) {
tnow += v[pos][brr[j]];
}
if (pos < n) {
DFS(pos + 1, cnt - 1, tnow);
} else {
ans = max(ans, tnow);
}
--arr[0];
}
//0
brr[++brr[0]] = pos;
tnow = now;
for (int i = 1; i <= arr[0]; ++i) {
tnow += v[arr[i]][pos];
}
if (pos < n) {
DFS(pos + 1, cnt, tnow);
} else {
ans = max(ans, tnow);
}
--brr[0];
}
int main() {
scanf("%d", &n);
m = n;
n <<= 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
scanf("%d", &v[i][j]);
}
}
DFS(1, m, 0ll);
printf("%lld\n", ans);
return 0;
}
H.Second Large Rectangle
题意:
求次大的全\(1\)矩阵。
思路:
先跑一遍最大全\(1\)矩阵,然后分别去掉四个角,再做\(4\)次,计算答案即可。
因为如果存在一个次大的全\(1\)矩阵,那么它肯定不包含已经求出的那个矩阵的某个角。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 1010
int n, m;
int G[N][N], T[N][N], l[N][N], r[N][N], up[N][N];
void get(int G[][N], int &x, int &y, int &row, int &col) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
l[i][j] = r[i][j] = j;
up[i][j] = 1;
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 2; j <= m; ++j) {
if (G[i][j] == 1 && G[i][j] == G[i][j - 1]) {
l[i][j] = l[i][j - 1];
}
}
}
for (int i = 1; i <= n; ++i) {
for (int j = m - 1; j >= 1; --j) {
if (G[i][j] == 1 && G[i][j] == G[i][j + 1]) {
r[i][j] = r[i][j + 1];
}
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (i > 1 && G[i][j] == 1 && G[i][j] == G[i - 1][j]) {
l[i][j] = max(l[i][j], l[i - 1][j]);
r[i][j] = min(r[i][j], r[i - 1][j]);
up[i][j] = up[i - 1][j] + 1;
}
if (G[i][j] == 1) {
int tcol = r[i][j] - l[i][j] + 1;
int trow = up[i][j];
if (tcol * trow > row * col) {
col = tcol;
row = trow;
x = i - up[i][j] + 1;
y = l[i][j];
}
}
}
}
}
int main() {
while (scanf("%d%d", &n, &m) != EOF) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
scanf("%1d", G[i] + j);
T[i][j] = G[i][j];
}
}
int x, y, tx, ty, row = 0, col = 0, trow = 0, tcol = 0;
get(G, x, y, row, col);
// cout << x << " " << y << " " << row << " " << col << endl;
if (row == 0) {
printf("0\n");
} else {
T[x][y] = 0;
get(T, tx, ty, trow, tcol);
T[x][y] = 1;
T[x + row - 1][y] = 0;
get(T, tx, ty, trow, tcol);
T[x + row - 1][y] = 1;
T[x][y + col - 1] = 0;
get(T, tx, ty, trow, tcol);
T[x][y + col - 1] = 1;
T[x + row - 1][y + col - 1] = 0;
get(T, tx, ty, trow, tcol);
printf("%d\n", trow * tcol);
}
}
return 0;
}
J.Subarray
题意:
有一场长度为\(10^9\)的\(01\)序列,保证序列中\(1\)的个数不超过\(10^7\),现在询问有多少子区间的区间和大于\(1\)。
思路:
考虑暴力怎么做,那么显然可以维护一个前缀和,然后记录一下前缀和出现的次数,然后枚举一个左区间,相当于找它右边的,前缀和大于等于它的个数。
但是序列长度为\(10^9\),显然不行。
但是注意到\(1\)的个数只有\(10^7\)个,那么有效的端点个数最多只有\(3 \cdot 10^7\),考虑最坏情况就是中间一段\(10^7\)个\(1\),然后两边各扩展一倍。
那么我们可以处理出有哪些端点是可能连续的,只需要对这些段分别处理就可以了。
考虑\(f[i]\)表示以第\(i\)个区间的右端点为结尾的最大区间和,转移有:
考虑\(g[i]\)表示以第\(i\)个区间的左端点为开头的最大区间和,转移有:
然后枚举\(i\),找到最远的\(j\),使得这一段的区间和大于\(0\)即可。
然后对这些段分别处理。
上面提到对段的处理中,先处理出前缀和次数的后缀和,然后加一个\(Lazy\)标记,往下转移即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define N 10000010
int n, l[1000010], r[1000010];
int f[N], g[N], sum[N * 3 + 10], b[N * 3 + 10], c[N * 3 + 10];
void solve() {
ll res = 0;
int i = 1, j, base = 10000000;
while (i <= n) {
j = i + 1;
while (j <= n && f[j - 1] + g[j] >= l[j] - r[j - 1] - 1) ++j;
--j;
int Left = max(0, l[i] - g[i]), Right = min(1000000000 - 1, r[j] + f[j]);
int Min = 1e9, Max = -1e9;
sum[0] = 0;
for (int k = Left, t = i; k <= Right; ++k) {
int now = k - Left + 1;
sum[now] = sum[now - 1];
if (k >= l[t] && k <= r[t]) {
++sum[now];
} else {
--sum[now];
}
if (k == r[t]) ++t;
Min = min(Min, sum[now] + base);
Max = max(Max, sum[now] + base);
++b[sum[now] + base];
}
for (int k = Max - 1; k >= Min; --k) {
b[k] += b[k + 1];
}
res += b[base + 1];
for (int k = Left; k <= Right; ++k) {
int now = sum[k - Left + 1] + base;
b[now + 1] -= c[now + 1];
c[now] += c[now + 1] + 1;
c[now + 1] = 0;
res += b[now + 1];
}
Min = max(0, Min - 10);
Max = min(30000000, Max + 10);
for (int k = Min; k <= Max; ++k) b[k] = c[k] = 0;
i = j + 1;
}
printf("%lld\n", res);
}
int main() {
while (scanf("%d", &n) != EOF) {
for (int i = 1; i <= n; ++i) {
scanf("%d%d", l + i, r + i);
}
//f[]表示以第i个区间的右端点为结尾的最大区间和是多少
f[1] = r[1] - l[1] + 1;
for (int i = 2; i <= n; ++i) {
f[i] = max(0, f[i - 1] - (l[i] - r[i - 1] - 1)) + r[i] - l[i] + 1;
}
//g[]表示以第i个区间的左端点为开头的最大区间和是多少
g[n] = r[n] - l[n] + 1;
for (int i = n - 1; i >= 1; --i) {
g[i] = max(0, g[i + 1] - (l[i + 1] - r[i] - 1)) + r[i] - l[i] + 1;
}
solve();
}
return 0;
}