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)\)的方案数。
那么就有如下转移:

\[\begin{eqnarray*} f[i][j] &=& \sum\limits_{k = 1}^j dp[i - 1][k] \quad \text{(i - 1)层的(k ~ j)这段区间没有墙} \\ &+& \sum\limits_{k = j + 1}^m dp[i - 1][k] \quad \text{(i - 1)层的(j ~ k)这段区间没有墙} \end{eqnarray*} \]

那么写成矩阵形式:

\[\begin{eqnarray*} \left[ \begin{array}{cccc} f_{i - 1, 1} \quad f_{i - 1, 2} \cdots, f_{i - 1, m} \end{array} \right] \cdot \left[ \begin{array}{cccc} A_{1, 1} \quad A_{2, 1} \cdots A_{m, 1} \\ A_{1, 2} \quad A_{2, 2} \cdots A_{m, 2} \\ \cdots \\ A_{1, m} \quad A_{2, m} \cdots A_{m, m} \end{array} \right] = \left[ \begin{array}{cccc} f_{i, 1} \quad f_{i, 2} \cdots f_{i, m} \end{array} \right] \end{eqnarray*} \]

其中\(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\)个区间的右端点为结尾的最大区间和,转移有:

\[\begin{eqnarray*} f[i] = max(0, f[i - 1] - (l[i] - r[i - 1] - 1)) + r[i] - l[i] + 1; \end{eqnarray*} \]

考虑\(g[i]\)表示以第\(i\)个区间的左端点为开头的最大区间和,转移有:

\[\begin{eqnarray*} g[i] = max(0, g[i + 1] - (l[i + 1] - r[i] - 1)) + r[i] - l[i] + 1 \end{eqnarray*} \]

然后枚举\(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;
}
posted @ 2019-07-21 09:15  Dup4  阅读(443)  评论(0编辑  收藏  举报