A 牛牛的方程式
Statement
牛牛最近对三元一次方程非常感兴趣。众所周知,三元一次方程至少需要三个方程组成一个方程组,才有可能得出一组解。
牛牛现在想要知道对于方程 中有没有至少存在一组 的解,且 都为整数,使得方程式成立。
裴蜀定理
对于方程 ,当且仅当 时方程有整数解。
题解
容易将上述定理拓展到多元情况。
当 时,有整数解。
但是当 时,方程存在两种情况:
对于 式,,对于 式, 无解。
如果不对 进行特判,会因为浮点错误 RE 掉 50 分。
代码
#include<bits/stdc++.h> using namespace std; #define int long long template < typename Tp > inline void read(Tp &x) { x = 0; int fh = 1; char ch = 1; while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if(ch == '-') fh = -1, ch = getchar(); while(ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar(); x *= fh; } template < typename Tp > inline void biread(Tp &x) { x = 0; int fh = 1; char ch = 1; while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if(ch == '-') fh = -1, ch = getchar(); while(ch >= '0' && ch <= '9') x = x * 2 + ch - '0', ch = getchar(); x *= fh; } int T; int a, b, c, d; inline void Init(void) { read(T); } inline void Work(void) { while(T--) { read(a); read(b); read(c); read(d); if(a == b && b == c && c == 0) { if(d == 0) puts("YES"); else puts("NO"); continue; } int p = __gcd(a, __gcd(b, c)); if(d % p == 0) puts("YES"); else puts("NO"); } } signed main(void) { Init(); Work(); return 0; }
B 牛牛的猜球游戏
Statement
牛牛和牛妹在玩猜球游戏,牛牛首先准备了 个小球,小球的编号从 。首先牛牛把这 个球按照从左到右编号为 的顺序摆在了桌子上,接下来牛牛把这 个球用 个不透明的杯子倒扣住。
牛牛接下来会按照一定的操作顺序以极快的速度交换这些杯子。
换完以后他问牛妹你看清楚从左到右的杯子中小球的编号了么?
由于牛妹的动态视力不是很好,所以她跑来向你求助。你在调查后发现牛牛置换杯子其实是有一定原则的。
具体来讲,牛牛有一个长度大小为 的操作序列。
操作序列的每一行表示一次操作都有两个非负整数 ,表示本次操作将会交换从左往右数第 个杯子和从左往右数第 个杯子( 和 均从 开始数)。请注意是换杯子,而不是直接交换 号球和 号球
牛牛和牛妹一共玩了 次猜球游戏,在每一轮游戏开始时,他都将杯子中的小球重置到从左往右依次为 的状态。
然后在第 轮游戏中牛牛会按照操作序列中的第 个操作开始做,一直做到第 个操作结束( 和 的编号从 1 开始计算)。
由于你提前搞到了牛牛的操作序列以及每一次游戏的 。请你帮助牛妹回答出牛牛每一轮游戏结束时,从左至右的杯子中小球的编号各是多少。
题解 - 线段树
对于区间 ,维护执行编号在 的操作的答案。
考虑如何合并左右子树答案。
实际上,假设右子树答案为
左子树答案为
考虑杯子编号,实际上,由于每一个询问,都是将杯子按照 的初始状态排列的,可以将杯子编号看作是杯子的相对位置。
将左子树对应位置的杯子按照右子树序列移动一下即可。
这实际上是一个序列置换,序列置换具有结合律,这也决定了应用线段树必须具有的区间可加性。
题解 - 预处理
同上解法一,因为序列置换具有的结合律,实际上先 模拟一遍并记录答案即可,并不需要线段树。
线段树代码
#include<bits/stdc++.h> using namespace std; template < typename Tp > inline void read(Tp &x) { x = 0; int fh = 1; char ch = 1; while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if(ch == '-') fh = -1, ch = getchar(); while(ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar(); x *= fh; } template < typename Tp > inline void biread(Tp &x) { x = 0; int fh = 1; char ch = 1; while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if(ch == '-') fh = -1, ch = getchar(); while(ch >= '0' && ch <= '9') x = x * 2 + ch - '0', ch = getchar(); x *= fh; } const int maxn = 100000 + 7; int n, T; int val[maxn * 4][10]; #define lfc (x << 1) #define rgc ((x << 1) | 1) int cp[10]; void build(int x, int l, int r) { if(l == r) { int p, q; read(p); read(q); val[x][0] = 0, val[x][1] = 1, val[x][2] = 2, val[x][3] = 3, val[x][4] = 4, val[x][5] = 5, val[x][6] = 6, val[x][7] = 7, val[x][8] = 8, val[x][9] = 9; swap(val[x][p], val[x][q]); return ; } int mid = (l + r) >> 1; build(lfc, l, mid); build(rgc, mid + 1, r); for(int i = 0; i < 10; i++) { val[x][i] = val[lfc][val[rgc][i]]; } } int ans[10]; void query(int x, int l, int r, int L, int R) { if(L <= l && r <= R) { memcpy(cp, ans, sizeof(ans)); for(int i = 0; i < 10; i++) { ans[i] = cp[val[x][i]]; } return ; } if(r < L || R < l) return ; int mid = (l + r) >> 1; query(lfc, l, mid, L, R); query(rgc, mid + 1, r, L, R); } inline void Init(void) { read(n); read(T); } inline void Work(void) { build(1, 1, n); // check done while(T--) { int l, r; read(l); read(r); ans[0] = 0, ans[1] = 1, ans[2] = 2, ans[3] = 3, ans[4] = 4, ans[5] = 5, ans[6] = 6, ans[7] = 7, ans[8] = 8, ans[9] = 9; query(1, 1, n, l, r); for(int i = 0; i <= 9; i++) printf("%d%c", ans[i], " \n"[i == 9]); } } signed main(void) { // freopen("B.in", "r", stdin); // freopen("test.out", "w", stdout); Init(); Work(); return 0; }
C 牛牛的凑数游戏
Statement
对于一个多重数集 ,对于一非负整数 ,若存在 且 中所有数字之和恰好等于 ,则说 可以表示 。
显然对于任意的多重数集都可以表示 ,因为空集是所有集合的子集。
牛牛定义集合 的最小不能表示数为,一个最小的非负整数 , 不能表示 。
举个例子来说,例如 ,那么集合 的最小不能表示数就为 。
因为子集 的和为 ,子集 的和为 ,子集 的和为 ,子集 的和为 ,子集 的和为 ,子集 的和为 ,子集 的和为 。
但是无法找到子集权值和恰好为 的子集,所以 无法表示。
现在有一个长度大小为 的正整数数组,牛牛每次选择一个区间 ,他想要知道假定给出的多重数集为 时,该集合的最小不能表示数是多少。
多重数集最小不可表示数
Theorem 设多重数集 , 将 中的数从小到大排列,其中第 小的数为 ,记 当 时集合 的最小不可表示数为
Prove
先证充分性。
只能由 加和得到,但 。
题解
迭代,主席树维护。
代码
D 牛牛的RPG游戏
Statement
牛牛最近在玩一款叫做“地牢迷宫”的游戏,该游戏中的每一层都可以看成是一个 的二维棋盘,牛牛从左上角起始的 点移动到右下角的 点。
游戏中的每一个格子都会触发一些事件,这些事件将会影响玩家的得分。
具体来说,每到一个格子玩家触发事件时,首先会立即获得一个收益得分 。注意这个得分不一定是正的,当它的值为负时将会扣除玩家一定的分数。
同时这个事件还会对玩家造成持续的影响,直到玩家下一次触发其他事件为止,每走一步,玩家都会获得上一个事件触发点 的得分。
在游戏开始时牛牛身上还没有任何的 ,所以在牛牛还未触发任何事件之前每走一步都不会产生任何影响。
牛牛使用“潜行者”这个职业,所以他路过地牢中的格子时,可以选择不去触发这些事件。
同时牛牛是一个速通玩家,想要快速的到达终点,所以他每次只会选择往右走或者往下走。
牛牛想要知道,他玩游戏可以获得的最大得分是多少,你能告诉他么。
CDQ 分治及其在 DP 方面的应用
可以参见本场出题人神仙的博客 https://blog.nowcoder.net/n/f44d4aada5a24f619442dd6ddffa7320
CDQ 主要能够对偏序形式限制的 DP 转移顺序,直接拍掉一维
题解
设 代表到 且触发其事件的最大收益。
因为终点的 和 都为 ,所以上述状态是正确的。
容易写出转移方程
且要求满足 两个限制条件
发现转移方程符合斜率优化形式,扒掉 并变形后得到
注意到转移顺序非常毒瘤,需要使用 cdq 分治将 的限制条件直接拍扁。
又注意到 和 不满足单调增,转移需要 cdq 分治 / 二分 / 李超线段树。
用命题人的话来说,算法 + 数据结构非常优美,我选李超树。
李超树用的 dp 式子就是上面那个暴力式子,而不是后面的斜率柿子,这也是考试时候 40 -> 10 的原因。
李超树形式的式子:
代码
#include<bits/stdc++.h> using namespace std; #define int long long template < typename Tp > inline void read(Tp &x) { x = 0; int fh = 1; char ch = 1; while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if(ch == '-') fh = -1, ch = getchar(); while(ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar(); x *= fh; } template < typename Tp > inline void biread(Tp &x) { x = 0; int fh = 1; char ch = 1; while(ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if(ch == '-') fh = -1, ch = getchar(); while(ch >= '0' && ch <= '9') x = x * 2 + ch - '0', ch = getchar(); x *= fh; } const int maxn = 500000 + 7; int n, m; int buff[maxn], v[maxn]; struct Interval { int id; int k, b; }val[5000000]; int lfc[5000000], rgc[5000000], cnt; int dp[maxn]; inline int id(int x, int y) { return (x - 1) * m + y; } inline void reserv(int k, int &x ,int &y) { x = k / m + 1, y = k % m; if(y == 0) y = m, x--; } inline int calc(Interval p, int x) { return p.k * x + p.b; } inline int New(){ ++cnt; lfc[cnt] = rgc[cnt] = val[cnt].id = val[cnt].k = val[cnt].b = 0; return cnt; } void modify(int x, int l, int r, int L, int R, Interval p) { int mid = (l + r) >> 1; if(L <= l && r <= R) { if(!val[x].id || calc(val[x], mid) < calc(p, mid)) swap(val[x], p); if(!p.id || p.k == val[x].k || l == r) return ; double cross = (double)(p.b - val[x].b) / (double)(val[x].k - p.k); if(cross < (double)l || cross > (double)r) return ; if(p.k < val[x].k) { if(!lfc[x]) lfc[x] = New(); modify(lfc[x], l, mid, L, R, p); } else { if(!rgc[x]) rgc[x] = New(); modify(rgc[x], mid + 1, r, L, R, p); } } else { if(L <= mid) { if(!lfc[x]) lfc[x] = New(); modify(lfc[x], l, mid, L, R, p); } if(R > mid) { if(!rgc[x]) rgc[x] = New(); modify(rgc[x], mid + 1, r, L, R, p); } } } Interval query(int x, int l, int r, int pos) { if(l == r) return val[x]; int mid = (l + r) >> 1; Interval res; if(pos <= mid) { if(!lfc[x]) lfc[x] = New(); res = query(lfc[x], l, mid, pos); } else { if(!rgc[x]) rgc[x] = New(); res = query(rgc[x], mid + 1, r, pos); } if(!res.id || calc(res, pos) < calc(val[x], pos)) return val[x]; return res; } void cdq(int l, int r) { if(l == r) { cnt = 0; New();// val[1].b = -0x3f3f3f3f3f3f3f3fll; Interval q; // if(l == 1) q.id = id(l, 1), q.k = buff[id(l, 1)], q.b = dp[id(l, 1)] - (l + 1) * buff[id(l, 1)]; modify(1, 1, m + n, 1, m + n, q); for(int i = 2; i <= m; i++) { Interval p = query(1, 1, n + m, (l + i)); int k = p.id, x, y; reserv(k, x, y); dp[id(l, i)] = max(dp[id(l, i)], dp[k] + (l + i - x - y) * buff[k] + v[id(l, i)]); p.id = id(l, i), p.k = buff[id(l, i)], p.b = dp[id(l, i)] - (l + i) * buff[id(l, i)]; modify(1, 1, n + m, 1, n + m, p); } return ; } int mid = (l + r) >> 1; cdq(l, mid); cnt = 0; New();// val[1].b = -0x3f3f3f3f3f3f3f3fll; for(int j = 1; j <= m; j++) { for(int i = l; i <= mid; i++) { Interval p; p.id = id(i, j), p.k = buff[id(i, j)], p.b = dp[id(i, j)] - (i + j) * buff[id(i, j)]; modify(1, 1, n + m, 1, n + m, p); } for(int i = mid + 1; i <= r; i++) { Interval p = query(1, 1, n + m, i + j); int k = p.id, x, y; reserv(k, x, y); dp[id(i, j)] = max(dp[id(i, j)], dp[k] + (i + j - x - y) * buff[k] + v[id(i, j)]); p.id = id(i, j), p.k = buff[id(i, j)], p.b = dp[id(i, j)] - (i + j) * buff[id(i, j)]; // modify(1, 1, n + m, 1, n + m, p); } } cdq(mid + 1, r); } inline void Init(void) { read(n); read(m); // for(int i = 1; i <= id()) for(int i = 1; i <= n; i++) { for(int j = 1; j <= m; j++) { read(buff[id(i, j)]); dp[id(i, j)] = -0x3f3f3f3f3f3f3f3fll; } } dp[1] = 0; for(int i = 1; i <= n; i++) { for(int j = 1; j <= m; j++) { read(v[id(i, j)]); } } } inline void Work(void) { cdq(1, n); printf("%lld\n", dp[id(n, m)]); } signed main(void) { Init(); Work(); return 0; }