传送门:[TJOI2018]数学计算
t a g tag tag:线段树维护操作序列
一开始看着这题好像没啥难度啊,直接模拟不就好了吗,为啥是蓝的呢?
仔细一想,哇塞,这数据范围,要开高精了吧。再稍微算一下,哇塞,直接模拟高精空间就要炸了啊。所以直接算高精是不现实的了。
再想想,一边算一边取模怎样呢,哇塞,有除法要算逆元欸。在看看数据,哇塞,模数不是质数欸。那么算逆元就变得相当麻烦了(而且还要特判整除)。
那么我们显然不可能就直接模拟正解了,所以我们考虑一个比较暴力的模拟。我们维护一个操作序列,然后如果是操作 1 1 1 就一直乘下去。每次遇到一个操作 2 2 2,就删掉前面操作序列中的那个数,然后从头开始再做一遍前缀积。
啊,这样一来,我们就考虑怎样能优化这个做法吧。想想刚才的暴力,肯定有很多重复计算,那么怎么避免呢。把前面算的数存下来?还是有很多无用功,再想想,维护一个数列,需要两个操作,第一个是把其中某一个数改成 1 1 1(就是题目中的操作 2 2 2),还有一个是在末尾加一个数(操作 1 1 1)。还需要从头到尾查前缀积,再抽象一点:支持单点修改,区间查询:线段树。
那么这样,我们就能用线段树维护一个操作序列,每当操作 1 1 1 的时候就在序列末尾加上一个 m m m,每当操作 2 2 2 的时候就把 p o s pos pos 位置的数字改成 1 1 1,这样每次操作之后输出前缀积就做完了。
#include
using namespace std;
#define int long long
#define in read()
#define MAXN 100100
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
inline int read(){
int x = 0; char c = getchar();
while(c < '0' or c > '9') c = getchar();
while('0' <= c and c <= '9'){
x = x * 10 + c - '0'; c = getchar();
}
return x;
}
int T = 0;
int q = 0; int M = 0;
int a[MAXN] = { 0 };
struct Tnode{
int dat;
int l, r;
}t[MAXN << 5];
inline void up(int p) { t[p].dat = (t[ls(p)].dat * t[rs(p)].dat % M) % M; }
void build(int p, int l, int r){
t[p].l = l, t[p].r = r;
if(l == r) { t[p].dat = 1; return; }
int mid = (l + r) >> 1;
build(ls(p), l, mid); build(rs(p), mid + 1, r);
up(p);
}
void update(int p, int idx, int val){
if(t[p].l == t[p].r) { t[p].dat = val; return; }
int mid = (t[p].l + t[p].r) >> 1;
if(idx <= mid) update(ls(p), idx, val);
else update(rs(p), idx, val);
up(p);
}
int query(int p, int l, int r){
if(l <= t[p].l and t[p].r <= r) return t[p].dat;
up(p);
int mid = (t[p].l + t[p].r) >> 1, ans = 0;
if(l <= mid) ans = (ans * query(ls(p), l, r) % M) % M;
if(r > mid) ans = (ans * query(rs(p), l, r) % M) % M;
return ans;
}
signed main(){
T = in;
while(T--){
q = in, M = in; build(1, 1, q);
for(int i = 1; i <= q; i++){
int op = in; a[i] = in;
if(op == 1){
update(1, i, a[i]);
cout << query(1, 1, q) << '\n';
}
else{
update(1, a[i], 1);
cout << query(1, 1, q) << '\n';
}
}
}
return 0;
}
t
a
g
tag
tag:神秘的分块??根号分治?? or 数学???
题目简单说来就是让我们用一个非严格单调递减的数列来拆分 n n n,问有多少种分法。
首先看到这题就一眼背包(完全背包),
O
(
n
2
)
O(n^2)
O(n2),看看,已经
70
p
t
s
70pts
70pts 了,感觉剩下 30pts 性价比好低啊,要不不做了吧qwq。记前
i
i
i 种数和为
j
j
j 的方案书为
f
i
,
j
f_{i, j}
fi,j,那么:
f i , j = f i , j − 1 + f i − 1 , j f_{i, j} = f_{i, j - 1} + f_{i - 1, j } fi,j=fi,j−1+fi−1,j
初始化 f 0 , 0 = 1 f_{0, 0} = 1 f0,0=1。再滚动数字组一下,甚至可以拿 80 p t s 80pts 80pts。
#include
using namespace std;
#define MAXN 101
int f[MAXN] = { 0 };
int n = 0; int p = 0;
int main(){
cin >> n >> p;
f[0] = 1;
for(int i = 1; i <= n; i++)
for(int j = i; j <= n; j++)
f[j] = (f[j - i] + f[j]) % p;
cout << f[n] << '\n';
return 0;
}
但是我们还是要追求正解的,所以我么考虑优化。
优化这个 d p dp dp 显然已经不能从转移入手了,已经是 O ( 1 ) O(1) O(1) 了还优化个寂寞。所以就只能从状态数入手。这样就要引入这道题的重点,也就是根号分治。
我们把所有数分成两部分,一部分是数值 < n < \sqrt n <n 的,另一部分是 ≥ n \geq \sqrt n ≥n 的。那么我们考虑把两部分分开计算再合并到一起。
最后就是一个比较简单的背包合并就好了。
#include
using namespace std;
#define MAXN 100100
#define MAXM 505
int n = 0; int p = 0;
int f[MAXN] = { 0 };
int g[MAXM][MAXN] = { 0 };
int main(){
cin >> n >> p; int ans = 0;
int m = sqrt(n) + 1;
f[0] = 1;
for(int i = 1; i < m; i++)
for(int j = i; j <= n; j++)
f[j] = (f[j - i] + f[j]) % p;
g[0][0] = 1;
for(int i = 1; i <= m; i++)
for(int j = i; j <= n; j++){
g[i][j] = (g[i][j] + g[i][j - i]) % p;
if(j >= m) g[i][j] = (g[i][j] + g[i - 1][j - m]) % p;
}
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
g[0][j] = (g[0][j] + g[i][j]) % p;
for(int i = 0; i <= n; i++)
ans = (ans + 1ll * g[0][i] * f[n - i] % p) % p;
cout << ans << '\n';
return 0;
}
我感觉我没怎么听懂…
传送门:AT2389 [AGC016E] Poor Turkeys
t a g tag tag:时光倒流???
正着做的话就是要满足一堆限制的情况下判断 ( i , j ) (i, j) (i,j) 合不合法,这样看起来就十分难做,所以我们考虑反着来。反着来的话就只需要满足 ( i , j ) (i, j) (i,j) 都存活的条件下判断前面一堆东西合不合法,看上去就会更加简单一些。
所以我们考虑如果只让 i i i 存活有哪些条件。我们从最后一个指令往前追溯,如果遇到的指令和 i i i 无关,比如一个 ( x , y ) (x, y) (x,y),那么这两只鸡就随便杀,因为他们俩的死活不会影响到 i i i。但是如果遇到一个指令是 ( i , j ) (i, j) (i,j),这里我们就不能杀 i i i,只能杀 j j j。但是 j j j 可能在之前的某一步就已经寄了,所以我们不仅要在这一步咔嚓掉 j j j,还要保证在之前的指令中 j j j 不会被咔嚓掉。那么就相当于在前面的指令中要求 i , j i, j i,j 都要存活。
于是我们就能想到维护一个 “保护集” S i S_i Si,表示要使得 i i i 最终存活下来,在前若干步中需要保护的鸡的集合,我们注意到,保护除了 i i i 之外的鸡都是为了在某一步把它干掉,所以集合中的元素能分成 i i i 和除了 i i i 的元素两部分。
同理,我们从 j j j 往上走也能维护出一个 “保护集” S j S_j Sj,如果我们要 ( i , j ) (i, j) (i,j) 都存活,那么我们就要求 S i ⋂ S j = Φ S_i\bigcap S_j = \Phi Si⋂Sj=Φ。
这样一来,我们就能 O ( n m ) O(nm) O(nm) 处理出所有鸡的 “保护集”,然后 O ( n 2 ) O(n^2) O(n2),枚举所有点对,然后每个点对取交集判断一下就搞定了。
#include
using namespace std;
#define in read()
#define MAXN 404
#define MAXM 100100
inline int read(){
int x = 0; char c = getchar();
while(c < '0' or c > '9') c = getchar();
while('0' <= c and c <= '9'){
x = x * 10 + c - '0'; c = getchar();
}
return x;
}
int n = 0; int m = 0;
struct Trequ{
int x, y;
}req[MAXM];
bitset<MAXN> s[MAXN];
bool flag[MAXN] = { 0 };
int main(){
n = in; m = in;
for(int i = 1; i <= m; i++) req[i].x = in, req[i].y = in;
for(int i = 1; i <= n; i++) s[i][i] = 1;
for(int i = 1; i <= n; i++)
for(int j = m; j >= 1; j--){
int x = req[j].x, y = req[j].y;
if(s[i][x] && s[i][y]) { flag[i] = 1; break; }
if(s[i][x] || s[i][y]) s[i][x] = s[i][y] = 1;
}
int ans = 0;
for(int i = 1; i <= n; i++){
if(flag[i]) continue;
for(int j = i + 1; j <= n; j++){
if(flag[j]) continue;
if((s[i] & s[j]).count() == 0) ans++;
}
}
cout << ans << '\n';
return 0;
}
传送门:AT3944 [ARC092C] Both Sides Merger
t a g tag tag:考虑答案的来源
遇到这种操作的题下不要慌,手玩几个样例找找规律。这里就不举例子了,我们可以发现,在这些点中,对答案产生贡献的就是奇或偶位上的子序列。 也就是说要么在奇数位上,要么在偶数位上,不可能同时在奇数和偶数位置上都对答案有贡献。
这样一来,我们就把问题转化成了一个最大子序列的问题,对于奇数位和偶数位分别跑一遍,然后再取 m a x max max 就做完了。
#include
using namespace std;
#define int long long
#define in read()
#define MAXN 1010
#define INFI 0x3f3f3f3f
inline int read(){
int x = 0; int f = 1; char c = getchar();
while(c < '0' or c > '9'){
if(c == '-') f = -1; c = getchar();
}
while('0' <= c and c <= '9'){
x = x * 10 + c - '0'; c = getchar();
}
return f * x;
}
int a[MAXN] = { 0 };
int n = 0; int m = 0;
int ans1 = 0, ans2 = 0;
int us1[MAXN], us2[MAXN];
int tot = 0;
int c[MAXN] = { 0 };
int from[MAXN] = { 0 };
void solve(int cnt, int us[]){
// for(int i = 1; i <= n; i++) cout << us[i] << ' '; puts("");
int temp = 0;
for(int i = 1; i <= n; i++)
if(us[i]) from[i] = temp, temp = i;
// for(int i = 1; i <= n; i++) cout << from[i] << ' '; puts("");
int l = 1, r = cnt;
for(int i = 1; i <= n; i++)
if(!us[i]) c[++tot] = 1, cnt--, l++;
else break;
for(int i = n; i >= 1; i--)
if(!us[i]) c[++tot] = cnt, cnt--, r--;
else break;
// for(int i = 1; i <= tot; i++) cout << c[i] << ' '; puts("");
int ll = l, rr = r;
for(int i = l; i <= r; i++){
// printf("i = %d\n", i);
if(!from[i]) continue;
// puts(" in");
int x = i - ll + 1, y = from[i] - ll + 1;
// printf(" x = %d y = %d\n", x, y);
while(x != y){
int mid = (x + y) >> 1;
c[++tot] = mid; x -= 2, ll += 2;
// printf("mid = %d\n", mid);
}
}
cout << tot << '\n';
for(int i = 1; i <= tot; i++) cout << c[i] << '\n';
}
signed main(){
n = in; int ans = -INFI, f = 0, all = 1;
for(int i = 1; i <= n; i++) { a[i] = in; if(a[i] > 0) all = 0; }
if(all == 1){
int ff = 0;
ans1 = -INFI, ans2 = -INFI;
for(int i = 1; i <= n; i += 2)
if(a[i] > ans1) ans1 = a[i], ff = i;
us1[ff] = 1;
for(int i = 2; i <= n; i += 2)
if(a[i] > ans2) ans2 = a[i], ff = i;
us2[ff] = 1;
}
else{
ans1 = 0, ans2 = 0;
for(int i = 1; i <= n; i += 2)
if(a[i] > 0) ans1 += a[i], us1[i] = 1;
for(int i = 2; i <= n; i += 2)
if(a[i] > 0) ans2 += a[i], us2[i] = 1;
}
if(ans1 > ans2) ans = ans1, f = 1;
else ans = ans2, f = 2;
cout << ans << '\n';
if(f == 1) solve(n, us1);
else if(f == 2) solve(n, us2);
return 0;
}