牛客小白月赛#28 题解
A.牛牛和牛可乐的赌约 #逆元 #快速幂
题目链接
题意
\(n\)面骰子(点数分别从\(1\)到\(n\),掷出每面的概率为\(\frac{1} {n}\)),先让你投\(m\)次骰子,如果全部投出点数为\(n\)点面。你在计算输的概率,请输出分数\(p/q\ mod\ 1e9+7\)。
分析
显然,P(输)\(=1- (\frac{1}{n})^m=\frac{(n-1)^m}{n^m}\),但注意题目要求你求出P(输)在\(mod\ 1e9+7\)意义下的值,故先对分子进行快速幂,再对分母\(n^m\)求出其逆元\(inv(n^m)\)。考虑到模数为质数,直接使用费马小定理+快速幂即可得到逆元。
费马小定理:若\(p\)为素数,\(a\)为正整数,且\(a、p\)互质,则有\(a^{p-1}\equiv 1(mod\ p)\)。
由于逆元\(x\)满足$a\times x \equiv 1(mod\ p)\ \Rightarrow\ a\times x \equiv a^{p-1}(mod\ p)\ \Rightarrow\ x\equiv a^{p-2}(mod\ p) $
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <cmath>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXN = 55;
ll qpow(ll base, ll power, ll mymod){
ll ans = 1;
while(power > 0){
if(power & 1)
ans = ans * base % mymod;
power >>= 1;
base = (base * base) % mymod;
}
return ans;
}
ll Inv(ll a, ll mymod){
return qpow(a, mymod - (ll)2, mymod);
}
int main() {
int q;
scanf("%d", &q);
while(q--){
ll n, m;
scanf("%lld%lld", &n, &m);
ll a = qpow(n, m, (ll)MOD);
ll b = Inv(a, (ll)MOD);
printf("%lld\n", ((ll)(a - 1) * b) % ((ll)MOD));
}
}
B.牛牛和牛可乐的赌约2 #博弈论 #打表
题目链接
题意
一个棋盘游戏,棋盘左上角为\((0,0)\),该棋盘在\((x, y)\)的位置有一棋子,你和\(Bob\)轮流移动该棋子,可左移也可上移,可移动一格或者两格,直到不能在移动(即到达\((0,0)\))的那个人为输。
你第一个出手,若游戏过程中两个人都用最优策略,最后你是否能够获胜。
分析
感谢@秃头小白的文章讲解。
显然\((0,0)\)为必败态。而若\((x,y)\)周围\((x−1,y)(x−2,y)(x,y−1)(x,y−2)\)存在一点为必败态,则该点是必胜态。
for (int i = 1; i <= 45; i++) {
for (int j = 1; j <= 45; j++) {
bool ans = 1;
//寻找必败点
if (i > 2) ans &= vis[i - 2][j];
if (j > 2) ans &= vis[i][j - 2];
if (i > 1) ans &= vis[i - 1][j];
if (j > 1) ans &= vis[i][j - 1];
vis[i][j] = !ans;
}
}
for (int i = 1; i <= 45; i++) {
for (int j = 1; j <= 45; j++) {
printf("%d ", vis[i][j]);
}
printf("\n");
}
通过打出P-N表格,我们就能观察本题隐藏的性质,注意到
该表格对\(3\times 3\)矩阵分形,预先处理\(3\times 3\)矩阵后,判断询问点在该矩阵位置即可。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <cmath>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXN = 55;
string str;
bool vis[MAXN][MAXN];
bool ans[4][4] = {{0, 1, 1}, {1, 0, 1}, {1, 1, 0}};
int main() {
int q;
scanf("%d", &q);
while(q--){
int i, j; scanf("%d%d", &i, &j);
if(ans[i % 3][j % 3]) printf("yyds\n");
else printf("awsl\n");
}
}
C.单词记忆方法 #递归 #分治
题目链接
题意
每个字母都有一个权值,\(A\)权值为\(1\),\(B\)权值为\(2\),… 依次类推。现要你求出一个字符串所有字母的权值总和,注意该字符串存在括号及数字,如\(ABCABC、(ABC)2、HHHH、(H2)2、H4\),形如化学式。
分析
其实这题就是CCF CSP201912-3的化学方程式的弱化版,只不过你不用判断等式两边是否相等,只需考虑一个化学式中有多少个原子即可。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <cmath>
#include <unordered_map>
using namespace std;
string str;
ll vis[4], ans;
ll getDigit(int& lo, int hi){
ll res = 0;
while(lo <= hi && isdigit(str[lo]))
res = res * (ll)10 + (ll)(str[lo++] - '0');
return res == 0 ? 1 : res;
}
void Compute(int lo, int hi, ll val){
if(lo == hi){
ans += (val * (ll)(str[lo] - 'A' + 1));
return;
}
val *= (ll)getDigit(lo, hi);
for (int i = lo, j = i + 1; i <= hi; i = j, j++){
if('A' <= str[i] && str[i] <= 'Z'){
int tmp = j;
Compute(i, j - 1, val * (ll)getDigit(tmp, hi));
}
else if(str[i] == '('){
for (int cnt = 1; cnt != 0; j++){
if(str[j] == '(') cnt++;
else if(str[j] == ')') cnt--;
}
int tmp = j;
Compute(i + 1, j - 1, val * (ll)getDigit(tmp, hi));
}
}
}
int main(){
cin >> str;
Compute(0, str.length(), 1);
printf("%lld\n", ans);
}
D.位运算之谜
题目链接
题意
\(a+b\)的值为\(x\),\(a\&b\)的值为\(y\),首先需要判断能否有一组\(a, b\)满足当前的情况,如果有,那么求出\(a\ xor\ b\),否则输出\(-1\)
分析
实际上该题并不需要我们将\(a,b\)的具体值求出来,我们知道异或和即是不进位的加法,我们现在知道\(a+b\)的结果,又因为\((a\&b)<<1\)模拟\(a+b\)的进位。正常的加法运算 \(a+b = a\ xor\ b+((a\&b)<<1)\)。
当\(a+b - ((a \& b ) << 1)\)如果是负数,显然无法存在\(a,b\)满足题意,因为\(a\ xor\ b\)一定不为负。另外,还有一个排除条件,由于\(a\&b\)与\(a\ xor\ b\)一定取反,也就说两者按位与一定为\(0\)。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <cmath>
#include <unordered_map>
using namespace std;
typedef long long ll;
ll x, y;
int main(){
int q; scanf("%d", &q);
while(q--){
scanf("%lld%lld", &x, &y);
ll ans = x - (ll)(y << 1);
if(ans < 0 || ans & y) printf("-1\n");
else if(ans >= 0) printf("%lld\n", ans);
}
}
F.牛牛的健身运动 #三分思想
题目链接
题意
\(m\)天中,牛牛选其中一天来锻炼。每天有\(n\)件器材。第\(k\)天的第\(i\)种器材能够增加\(k*a_i+b_i\)力量。由于某种原因,牛牛只能在当天选择所有器材中增加力量值最小的两件(且两件不能相同)。牛牛希望能够选择某一天,使得他增加力量值最大,现要你求出他最多增加的力量。
分析
每一天牛牛只能选择力量值最小的两件,也就说假定现在为第\(k\)天,则他只能增加\(y_k=min(k*(a_i+a_j)+(b_i+b_j), 1\leq i,j \leq n)\)力量值,显然需要枚举两两器材搭配取得这一最小值。
但牛牛又希望从所有天中的最小值\(y_k(1\leq k \leq m)\)取得最大值,求给定区间的最大值,用三分查找的算法较为适合。
#include <string>
#include <cstring>
#include <cstdio>
#include <iostream>
#include <stack>
#include <queue>
#include <map>
#include <vector>
#include <deque>
#include <cmath>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MAXN = 2e3 + 5;
int n, m;
ll a[MAXN], b[MAXN];
ll Check(int x) { //获得当天增加力量最小值
ll mymin = 0x3f3f3f3f3f3f3f3f;
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++)
mymin = min(mymin, (a[i] + a[j]) * (ll)x + (b[i] + b[j]));
return mymin;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%lld%lld", &a[i], &b[i]);
int lo = 1, hi = m;
ll ans = -0x3f3f3f3f3f3f3f3f;
while (lo + 10 < hi) { //缩小区间
int mid1 = lo + (hi - lo) / 3;
int mid2 = hi - (hi - lo) / 3;
if (Check(mid1) <= Check(mid2)) lo = mid1;
else hi = mid2;
}
for (int i = lo; i <= hi; i++) ans = max(ans, Check(i)); //在小区间枚举比较最值
printf("%lld\n", ans);
return 0;
}
G.牛牛和字符串的日常 #KMP
题目链接
题意
给定喜欢句子,如果今日读的书中某连续\(k\)个字符恰好为喜欢句子的某个前缀,那么能够加\(k\)分。然而每天只能注意到一次自己喜欢的句子(即只能加分一次),你需要尽可能找到加分高的句子。现要求\(n\)天后总共最多能有多少分
分析
由数据规模可知只能满足\(O(n)\)或更低的算法。直接用KMP模板即可,求出模式串(即喜欢句子)最长匹配长度。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <cmath>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXN = 1e5 + 5;
int nextTable[MAXN];
void buildNext(string str){
int comlen = nextTable[0] = -1;
int len = str.length(), i = 0;
while(i < (len - 1)){
if(comlen == -1 || str[i] == str[comlen]){
i++;
comlen++;
nextTable[i] = comlen;
}
else comlen = nextTable[comlen];
}
}
int KMP(string str, string T){
int i = 0, j = 0;
int res = -1;
while(i < (int)T.length() && j < (int)str.length()){
if(j < 0 || T[i] == str[j]){
i++; j++;
}
else j = nextTable[j];
res = max(res, j); //需添加的语句,更新模式串最长匹配长度
}
return res;
}
int main(){
string str; cin >> str; buildNext(str);
int n, ans = 0; scanf("%d", &n);
for (int i = 1; i <= n; i++){
string tmp; cin >> tmp;
ans += KMP(str, tmp);
}
printf("%d\n", ans);
}
H.上学要迟到了 #单源最短路径
题目链接
题意
你去学校的路是一条总共有\(n\)个公交车站的直线,且车站之间距离相等,每个车站只有一种对应的公交车\(a_i\),且每个公交车只在对应站停下。而第\(i\)种公交车到达其下一个对应车站所需时间为\(t_i\),且单向行驶(从左到右)。而走路可以任意方向走,但步行走一站需\(T\)时间。你的家在\(s\),学校在\(t\),求到达学校最短时间。
分析
这题其实不难,但是要注意两个隐藏细节(wa了好多发):
- 虽然公交车是单向,但人步行可以双向!
- 公交车从一个对应站到另一个对应站,无论距离多长,耗费时间固定!
接下来,给人步行路径建双向边,给每种车的途径站建单向边,用\(Dijkstra\)算法求出路径即可。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <cmath>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXN = 1e5 + 5;
typedef struct Edge{
int to, w;
} Edge;
typedef struct qnode{
int v, data;
bool operator <(const qnode& r) const {
return data > r.data;
}
}qnode;
vector<Edge> H[MAXN];
int n, m, st, ed, t;
int cost[MAXN], lastPos[MAXN];
int dis[MAXN];
void dijkstra(){
priority_queue<qnode> myque;
for (int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f;
dis[st] = 0;
myque.push({st, 0});
while(!myque.empty()){
qnode tmp = myque.top(); myque.pop();
int u = tmp.v, len = tmp.data;
if(len != dis[u]) continue;
for (int i = 0; i < H[u].size(); i++){
int v = H[u][i].to, w = H[u][i].w;
if(dis[u] + w < dis[v]){
dis[v] = dis[u] + w;
myque.push({v, dis[v]});
}
}
}
}
int main(){
cin >> n >> m >> st >> ed >> t;
for (int i = 1; i <= m; i++) scanf("%d", &cost[i]);
for(int i = 1, v; i <= n; i++){
scanf("%d", &v);
if(lastPos[v] != 0) H[lastPos[v]].push_back({i, cost[v]});
lastPos[v] = i;//记录v车的上一站点
}
for (int i = 1; i <= n - 1; i++) H[i].push_back({i + 1, t});
for (int i = n; i >= 2; i--) H[i].push_back({i - 1, t}); //双向边
dijkstra();
printf("%d\n", dis[ed]);
return 0;
}
I.迷宫 #动态规划
题目链接
题意
\(n\times m(n,m\leq 100)\)地图,每个点有权值\(a_{ij}\),现要你从\((1,1)\)走到\((n, m)\),可以往右走或往下走,每经过一个点便可得到相应权值,对权值和\(mod\ 1e4+7\)。问你从不同方式走到\((n,m)\)能获得多少种权值和?
分析
如果暴力搜索的话,总共有\(C^n_{m+n}\)路径,估计时间复杂度为\(O(n!)\),显然要用DP。
注意到取模\(1e4+7\),我们知道权值和最大可能为\(1e4+7\),最小可能为\(1\),说明我们可以把权值枚举出来的。但是我们不知道能取到这一区间的哪些值,所以需要DP下看看哪些模数对应的权值和是可达的。
于是定义\(dp[i][j][k]\)表示,地图\((i,j)\)点,能否达到权值\(k\)。接下来枚举地图上的每一点及其所有模数,转移可从上一行的点、上一列的点进行。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <cmath>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MOD = 1e4 + 7;
const int MAXN = 105;
int n, m, val[MAXN][MAXN];
bool dp[MAXN][MAXN][MOD + 5];
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
int tmp; scanf("%d", &tmp);
val[i][j] = tmp % MOD; //避免
}
}
dp[1][1][val[1][1]] = 1;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
for (int k = 0; k < MOD; k++){ //枚举模数
dp[i][j][k] |= dp[i - 1][j][((k - val[i][j]) % MOD + MOD) % MOD];
dp[i][j][k] |= dp[i][j - 1][((k - val[i][j]) % MOD + MOD) % MOD];
}
}
}
int ans = 0;
for (int k = 0; k < MOD; k++) ans += dp[n][m][k];
printf("%d\n", ans);
}
J.树上行走 #并查集
题目链接
题意
给定\(n\)个节点、\(n-1\)条边的树,它有两种类型的点,一旦进入一个点,你只能走到与之有边相连的且与之同类型节点,现要你尽量在更多的节点可以移动,可以进入哪些点?
分析
先对同类型节点进行合并成树,然后对每种类型节点的树的规模进行比较,找到其最值,最后再对规模等于该最值的类型节点加入答案。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stack>
#include <string>
#include <cstring>
#include <vector>
#include <cmath>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXN = 2e5 + 5;
int flag[MAXN], tot = 0, fa[MAXN];
int n, sz[MAXN], ans[MAXN];
int findSet(int x){
if(x != fa[x]) fa[x] = findSet(fa[x]);
return fa[x];
}
int main(){
scanf("%d", &n);
for (int i = 1, tmp; i <= n; i++){
scanf("%d", &flag[i]);
fa[i] = i;
}
for (int i = 1, u, v; i <= n - 1; i++){
scanf("%d%d", &u, &v);
if(flag[u] != flag[v]) continue;
int pu = findSet(u), pv = findSet(v);
fa[pu] = pv; //同类型节点进行合并
}
int mymax = -1, cnt = 0;
for (int i = 1; i <= n; i++) findSet(i); //一定要保证所有顶点的路径都压缩完!!!
for (int i = 1; i <= n; i++){
sz[fa[i]]++; //统计该类型节点组成的树规模
mymax = max(mymax, sz[fa[i]]);
}
for (int i = 1; i <= n; i++){
if(sz[fa[i]] == mymax) ans[++cnt] = i; //满足条件,加入答案
}
printf("%d\n", cnt);
for (int i = 1; i <= cnt; i++) printf("%d ", ans[i]);
return 0;
}