周一 5.3(五一集训)
HDU 6186(前缀+后缀)
这道题比赛时直接用线段树写的,比较麻烦
有个比较简单的思路,就是配合前缀和后缀
取区间的一个点,然后要处理这个点前后的信息的时候,可以预处理出前缀后缀
#include <bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int a[N], n, q, t;
int s1[N], s2[N], s3[N], s4[N];
int main()
{
while(~scanf("%d%d", &n, &q))
{
_for(i, 1, n) scanf("%d", &a[i]);
t = a[1]; _for(i, 2, n) t ^= a[i];
s1[1] = a[1]; _for(i, 2, n) s1[i] = s1[i - 1] & a[i];
s2[n] = a[n]; for(int i = n - 1; i >= 1; i--) s2[i] = s2[i + 1] & a[i];
s3[1] = a[1]; _for(i, 2, n) s3[i] = s3[i - 1] | a[i];
s4[n] = a[n]; for(int i = n - 1; i >= 1; i--) s4[i] = s4[i + 1] | a[i];
while(q--)
{
int x; scanf("%d", &x);
if(x == 1) printf("%d %d %d\n", s2[2], s4[2], t ^ a[x]);
else if(x == n) printf("%d %d %d\n", s1[n - 1], s3[n - 1], t ^ a[x]);
else printf("%d %d %d\n", s1[x - 1] & s2[x + 1], s3[x - 1] | s4[x + 1], t ^ a[x]);
}
}
return 0;
}
HDU 5241(猜结论 + 高精度)
考试的时候我推了2个小时,还是没推出来
后来再回来看这道题,猜了个结论,高精度预处理一下,过了
还是大胆猜结论把,一开始很多人过了,很可能就是直接猜了个结论,不会是经过很复杂的推理的
听同学讲,每种语言相互独立,如果一种语言的方案数是a,那么答案就是a的n次方
由样例得a=32 太秀了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 5000;
int a[N][N], cnt[N], n; //n = i 时的 第j位
int main()
{
cnt[0] = 1; a[0][1] = 1;
_for(n, 1, 3000)
{
cnt[n] = cnt[n - 1];
_for(i, 1, cnt[n - 1]) a[n][i] = a[n - 1][i] * 32;
_for(i, 1, cnt[n])
{
a[n][i + 1] += a[n][i] / 10;
a[n][i] %= 10;
}
while(a[n][cnt[n] + 1])
{
cnt[n]++;
a[n][cnt[n] + 1] += a[n][cnt[n]] / 10;
a[n][cnt[n]] %= 10;
}
}
int T, kase = 0; scanf("%d", &T);
while(T--)
{
int n; scanf("%d", &n);
printf("Case #%d: ", ++kase);
for(int i = cnt[n]; i >= 1; i--) printf("%d", a[n][i]);
puts("");
}
return 0;
}
【模板】可持久化线段树 2(主席树)
学一下主席树,昨天有一道可持续化Trie的题目
最近补一补可持续化数据结构
主席树
也叫可持久化线段树,可以保存线段树的历史信息
主席树用来处理区间第k大的问题
首先先考虑用线段树处理一个区间内第k大的数
我们根据数的权值,注意不是下标,来建立线段树
类似权值树状数组
如果数据很大就要提前离散化一下
然后线段树的区间l到r表示值l到r有多少数
比如2 到 4 当前的区间是1 2 3 那么2到4这个区间就有2个数(2, 3)
建立出这样一颗线段树后,查询的时候可以得到左右儿子区间的个数
如果左区间出现次数大于等于k,那就往左边递归,如果小于k,说明这个
第k大的数在右区间,就往右递归,如果左儿子的值为x,那么向右递归的时候第k大要改成第(k-x)大
这样一直递归下去,到叶子的时候这个值就是所求答案
所以这就是用线段树的思路,但是如果对每一个询问的区间都这样建立线段树时间空间都爆炸
考虑怎么优化
首先是一个常见思路,把区间化为前缀和的差值
注意这个区间和前面说的那个区间不一样,前面说的那个是值的区间,现在说的这个是下标的区间
这两个非常容易弄混
区间l到r,化为[1, r] - [1, l - 1]
这个时候是符合情况的,[1, r]出现5次,[1, l - 1]出现2次,那么[l, r]就是3次,相减就好了
所以我们就建立n颗线段树,第i颗线段树表示下标从1到i的线段树
这个时候要建立n颗线段树,空间会炸,继续考虑怎么优化
可以发现第i颗线段树和第i-1颗线段树相比,只是插入了a[i]而已,只有一条链不一样,大部分是一样的
所以我们可以利用之前的结果,建立新线段树的时候,一样的部分直接复制
具体的实现方法是儿子的坐标直接和前一棵树一样
然后一开始要建立一颗空树,后面加入的树在这棵树的基础上。
然后注意是动态开点,空间直接左移5,开大一些
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e5 + 10;
int s[N << 5] , root[N << 5], ls[N << 5], rs[N << 5]; //空间要开大一些,直接左移5位
int a[N], lsh[N], n, m, cnt, len;
void build(int& k, int l, int r) //注意动态开点。先建立第0个版本的空树
{
k = ++cnt;
if(l == r) return;
int m = l + r >> 1;
build(ls[k], l, m);
build(rs[k], m + 1, r);
}
void add(int& k, int pre, int x, int l, int r) //新建和查询的时候要有2个根节点,当前的和历史的
{
k = ++cnt;
ls[k] = ls[pre]; rs[k] = rs[pre]; s[k] = s[pre] + 1; //复制前一个版本线段树的信息
if(l == r) return; //到叶子节点就返回
int m = l + r >> 1;
if(x <= m) add(ls[k], ls[pre], x, l, m); //跳的时候当前和之前的都要跳
else add(rs[k], rs[pre], x, m + 1, r);
}
int query(int pre, int now, int l, int r, int num)
{
if(l == r) return l;
int m = l + r >> 1, x = s[ls[now]] - s[ls[pre]]; //得到左区间内数的个数
if(x >= num) return query(ls[pre], ls[now], l, m, num);
else return query(rs[pre], rs[now], m + 1, r, num - x); //注意这里num - x
}
void init() //离散化
{
sort(lsh + 1, lsh + n + 1);
len = unique(lsh + 1, lsh + n + 1) - lsh - 1; //多减一
_for(i, 1, n) a[i] = lower_bound(lsh + 1, lsh + len + 1, a[i]) - lsh;
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n)
{
scanf("%d", &a[i]);
lsh[i] = a[i];
}
init();
build(root[0], 1, len); //注意这里右端点是离散化后的值,不是n
_for(i, 1, n) add(root[i], root[i - 1], a[i], 1, len); //建立第i版本的线段树,插入点
while(m--)
{
int l, r, x;
scanf("%d%d%d", &l, &r, &x);
printf("%d\n", lsh[query(root[l - 1], root[r], 1, len, x)]); //注意是l - 1
}
return 0;
}
P4735 最大异或和(可持久化Trie)
慢慢体会这种可持久化的思想
首先把题目条件转化为s[n] ^ s[p - 1] ^ x
s[n] ^ x固定,p-1在l-1, r - 1中
所以思路就是把[l - 1, r - 1]的数建字典树,注意这里是s[i] 不是a[i]
然后就利用前缀和的思想,类似主席树,就是建立i颗字典树,表示区间1到i的字典树
然后要区间[l, r]的字典树就用[1, r] - [1, l - 1]的就行了
具体实现上其实就是看这个节点存不存在,所以用vis[i]表示这个节点被访问了几次,如果次数相减大于0那就是在这个区间内节点是存在的
然后这题有个坑,就是边界条件
按照公式,s[0] = 0
所以一开始是要加入s[0]的
如果是区间[1, x]那么减去root[0]。
如果[0, x]那么就依然是减去0,不可能是减去root[-1]
还有注意空间要开大一些,这种可持久化数据结构动态开点,空间大
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 6e5 + 10;
int t[N * 30][2], a[N], s[N], root[N];
int vis[N * 30], cnt, n, m;
void add(int now, int pre, int x) //建立新树注意复制前面的信息,在两颗树上跑
{
int p = now;
for(int i = 31; i >= 0; i--)
{
int idx = (x >> i) & 1;
t[p][idx] = ++cnt; //这里不用判断,一定是新建节点
t[p][idx ^ 1] = t[pre][idx ^ 1]; //复制前一颗树的信息
p = t[p][idx]; pre = t[pre][idx];
vis[p] = vis[pre] + 1; //根节点不用处理sum 因为询问的时候用不到。sum存当前这个节点访问过的次数
}
}
int query(int now, int pre, int x)
{
int p = now, res = 0;
for(int i = 31; i >= 0; i--)
{
int idx = (x >> i) & 1;
if(vis[t[p][idx ^ 1]] > vis[t[pre][idx ^ 1]]) //作差来判断是否存在。其他是一样的
{
res |= 1 << i;
p = t[p][idx ^ 1]; pre = t[pre][idx ^ 1];
}
else p = t[p][idx], pre = t[pre][idx]; //在两颗Trie树上跑,相减得当前得Trie
}
return res;
}
int main()
{
add(root[0] = ++cnt, 0, 0); //按照公式s[0] = 0的,这个点要加入,否则会WA
scanf("%d%d", &n, &m); //一般来说也会建一个空树
_for(i, 1, n)
{
scanf("%d", &a[i]);
s[i] = s[i - 1] ^ a[i];
add(root[i] = ++cnt, root[i - 1], s[i]); //给root分配一个新编号
}
while(m--)
{
char op[5];
scanf("%s", op);
if(op[0] == 'A')
{
int x; scanf("%d", &x);
a[++n] = x;
s[n] = a[n] ^ s[n - 1];
add(root[n] = ++cnt, root[n - 1], s[n]);
}
else
{
int l, r, x;
scanf("%d%d%d", &l, &r, &x);
l--; r--;
if(l == 0) printf("%d\n", query(root[r], 0, s[n] ^ x));
else printf("%d\n", query(root[r], root[l - 1], s[n] ^ x));
}
}
return 0;
}
poj 6191(dfs序 + 可持久化Trie)
首先用dfs序把子树转化到区间上
问题就转化为询问区间[l, r]上,一个数和区间中的数异或最大是多少
显然就是将[l, r]上的数建立字典树
同样用前缀和的思想,建立可持久化字典树
在dfs的区间上建立可持久化字典树,每次就加入区间上这个点的字典树
然后关于一开始要不要加入0的问题,看题目情况
加入0是很有意义的,意味着多了一个数
不加就正常情况,正常情况下减去root[0]是没事的,因为root[0]是空的
如果加入0就意味着root[0]加入了0,这两种情况不一样
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int t[N * 35][2], val[N], root[N], vis[N * 35];
int L[N], R[N], v[N], id, n, q, cnt;
vector<int> g[N];
void add(int p, int pre, int x)
{
for(int i = 31; i >= 0; i--)
{
int idx = (x >> i) & 1;
t[p][idx] = ++cnt;
t[p][idx ^ 1] = t[pre][idx ^ 1];
p = t[p][idx]; pre = t[pre][idx];
vis[p] = vis[pre] + 1; //不要忘记写
}
}
int query(int p, int pre, int x)
{
int res = 0;
for(int i = 31; i >= 0; i--)
{
int idx = (x >> i) & 1;
if(vis[t[p][idx ^ 1]] > vis[t[pre][idx ^ 1]])
{
res |= 1 << i;
p = t[p][idx ^ 1]; pre = t[pre][idx ^ 1];
}
else p = t[p][idx], pre = t[pre][idx]; //这里原来我的,写成;查了一个半小时……
}
return res;
}
void dfs(int u)
{
L[u] = ++id;
v[id] = val[u]; //把值记录一下
for(auto x: g[u]) dfs(x);
R[u] = id;
}
int main()
{
while(~scanf("%d%d", &n, &q))
{
cnt = id = 0;
memset(t, 0, sizeof(t));
memset(vis, 0, sizeof(vis));
_for(i, 1, n)
{
scanf("%d", &val[i]);
g[i].clear();
}
_for(i, 1, n - 1)
{
int x; scanf("%d", &x);
g[x].push_back(i + 1);
}
dfs(1); //不要随便加0节点 看题目情况,加上0意味着又和0异或的情况
_for(i, 1, n) add(root[i] = ++cnt, root[i - 1], v[i]); //注意是v[i]
while(q--)
{
int u, x;
scanf("%d%d", &u, &x);
printf("%d\n", query(root[R[u]], root[L[u] - 1], x));
}
}
return 0;
}
HDU 5245(数学期望)
数学期望的题我几乎没做过,导致完全没有思路
思路要从定义出发
数学期望的定义是每种情况乘上概率的总和,这是高中数学
所以每种情况到这道题里面,就可以考虑每个方格被覆盖的概率
对于每个方格,就可以考虑一共有多少种情况被覆盖 除以所有情况就是概率
这里反过来想方便很多,考虑多少种情况不被覆盖
k次有没有被覆盖,那就考虑k次没有被覆盖
如果单次没被覆盖的概率是p
那么k次后被覆盖的概率就是1 - p ^ k
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll; //注意开long long
ll f(ll x) { return x * x; }
int main()
{
int T, kase = 0; scanf("%d", &T);
while(T--)
{
ll n, m; int k;
scanf("%lld%lld%d", &n, &m, &k);
double ans = 0;
_for(i, 1, n)
_for(j, 1, m)
{
ll sum = 0; //不包含有多少种可能性
sum += f(n * (j - 1)) + f(n * (m - j));
sum += f(m * (i - 1)) + f(m * (n - i));
sum -= f((i - 1) * (j - 1)) + f((n - i) * (j - 1));
sum -= f((i - 1) * (m - j)) + f((n - i) * (m - j));
double t = 1.0 * sum / f(n * m);
ans += 1 - pow(t, k);
}
printf("Case #%d: %.0f\n", ++kase, ans); //四舍五入就%.0f
}
return 0;
}
周二 5.4(五一集训)
HDU 5242(dfs + 贪心)
这是昨天训练赛的题目
这道题有个很显然的思路,就是每次都选择当前可以选择的最长的链
但是这涉及到删除已经选的链,同时又要找剩下的最长链,就不知道怎么实现
然而有个非常非常巧妙的思路可以正好完成这个过程
dfs的时候维护最长链,对于那些不是最长链的儿子就把它的链的值加入优先队列,这个过程其实就是断开的过程
当然现在的这条最长链在后面也可能被断掉
最后处理完后,就正好断成了一条一条链,加入优先队列中,贪心取就好了
这个切链的方式非常巧妙
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
priority_queue<ll> q;
vector<int> g[N];
int v[N], n, k;
ll dfs(int u)
{
ll mx = 0;
for(auto v: g[u])
{
ll t = dfs(v);
if(t > mx)
{
if(mx) q.push(mx); //mx有值则加入
mx = t;
}
else q.push(t);
}
return mx + v[u];
}
int main()
{
int T, kase = 0;
scanf("%d", &T);
while(T--)
{
scanf("%d%d", &n, &k);
_for(i, 1, n) scanf("%d", &v[i]), g[i].clear();
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
}
while(!q.empty()) q.pop();
ll ans = dfs(1); k--;
while(k && !q.empty())
{
ans += q.top();
q.pop();
k--;
}
printf("Case #%d: %lld\n", ++kase, ans);
}
return 0;
}
HDU 6278(主席树)
对于一个序列,定义一个数h
对于一个数t,在此序列中大于等于t的数的个数大等于t
h是最大的t 有点绕
每次询问一个区间里面的t
一开始我想的是莫队,二分,树状数组啥的,但是卡住了,就没继续往这个思路想 但是这个思路是可以做的,有同学按照这个思路AC了
然后我就想到了前几天刚学的主席树,我还不是很熟练主席树
思考方式是,先考虑对当前这个区间建立权值线段树,记录区间内数出现的次数
计算出右区间有多少个数,然后把右区间的左端点当作一个可能的答案,如果右区间的次数>=左端点,那就往右递归
否则往左递归,同时右区间的次数要加上。一直递归到叶子就是答案
对主席树有新的理解,对于第k大的数和这道题,我发现区间都是满足某种单调性
对于第k大的数,从右到左,数出现的次数是越来越多的
所以可以通过一个有点像二分的方式,每次往下区间都减半,去寻找答案
对于这道题,其实就是要满足 出现次数 >= 本身的值
也就是 出现次数 - 本身的值>= 0 我比赛时就是发现这个值从右到左是一直递增的,因为出现次数一直变大,本身的值一直变小
所以如果按照权值,每个权值对应的点记录一次出现次数,这样这个区间就是单调的,就可以用主席树
总结一下,就是以权值建立线段树,询问的每个区间对于题目要求的信息有单调性,就用主席树
有点类似每次选一个区间做二分
最后我之前一直纠结要不要建立空树的问题,貌似不写问题也不大
主席树的代码其实挺短的,比普通线段树要短,但思维量大一些
我看了网上的做法,我发现我的做法简单很多,直接主席树就好了,不需要再二分答案啥的
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int s[N << 5], ls[N << 5], rs[N << 5];
int root[N], cnt, n, q;
void add(int& k, int pre, int l, int r, int x)
{
k = ++cnt;
ls[k] = ls[pre]; rs[k] = rs[pre]; s[k] = s[pre] + 1;
if(l == r) return;
int m = l + r >> 1;
if(x <= m) add(ls[k], ls[pre], l, m, x);
else add(rs[k], rs[pre], m + 1, r, x);
}
int query(int k, int pre, int l, int r, int sum)
{
if(l == r) return l;
int r_sum = sum + s[rs[k]] - s[rs[pre]], m = l + r >> 1;
if(r_sum >= (m + 1)) return query(rs[k], rs[pre], m + 1, r, sum);
else return query(ls[k], ls[pre], l, m, r_sum);
}
int main()
{
while(~scanf("%d%d", &n, &q))
{
memset(s, 0, sizeof(s));
cnt = 0;
_for(i, 1, n)
{
int x; scanf("%d", &x);
add(root[i], root[i - 1], 1, 1e5, x);
}
while(q--)
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", query(root[r], root[l - 1], 1, 1e5, 0));
}
}
return 0;
}
HDU 6278(莫队 + 树状数组 + 二分答案)
我一开始是想这个思路的,但是比赛时就差最后一步二分答案想到,就换成主席树的思路了
这个思路时间复杂度挺高的n根号n logn 这道题3s
我写了个加奇偶优化是2s,然后我想试一下优化效果,去掉之后直接2.9s
奇偶优化还是很明显的
讲一下这个做法
首先是莫队,然后对权值建立树状数组
关键一步是在1到1e5这个区间二分答案,是对权值区间二分答案,而不是处理当前莫队的这个区间
我当时考虑的时候就关注点在莫队的这个区间,我在想怎么使它有序,因为有序之后可以二分找
直接排序必超时,如果用set维护就可以保持有序,但是又不能二分,set的lower_bound不行。
然后就卡住了,以为这个思路不行。
有序性的问题,如果把值弄到权值区间上就是有序的
我还尝试了一下二分答案 + 分块的做法,T了,分块还是时间复杂度比较大
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
struct query
{
int l, r, bl, id;
}q[N];
int a[N], ans[N], f[N], n, m;
int lowbit(int x) { return x & (-x); }
void modify(int x, int p)
{
for(; x <= 1e5; x += lowbit(x))
f[x] += p;
}
int sum(int x)
{
int res = 0;
for(; x; x -= lowbit(x))
res += f[x];
return res;
}
void add(int x) { modify(a[x], 1); }
void del(int x) { modify(a[x], -1); }
bool check(int x) { return x <= sum(1e5) - sum(x - 1); }
int work()
{
int l = 1, r = 1e5 + 1;
while(l + 1 < r)
{
int m = l + r >> 1;
if(check(m)) l = m;
else r = m;
}
return l;
}
bool cmp(query x, query y)
{
if(x.bl != y.bl) return x.bl < y.bl;
if(x.bl & 1) return x.r < y.r;
return x.r > y.r;
}
int main()
{
while(~scanf("%d%d", &n, &m))
{
memset(f, 0, sizeof(f));
_for(i, 1, n) scanf("%d", &a[i]);
int block = sqrt(n);
_for(i, 1, m)
{
int l, r;
scanf("%d%d", &l, &r);
q[i] = {l, r, l / block, i};
}
sort(q + 1, q + m + 1, cmp);
int l = 1, r = 0;
_for(i, 1, m)
{
int ll = q[i].l, rr = q[i].r;
while(l < ll) del(l++);
while(l > ll) add(--l);
while(r < rr) add(++r);
while(r > rr) del(r--);
ans[q[i].id] = work();
}
_for(i, 1, m) printf("%d\n", ans[i]);
}
return 0;
}
HDU 6285(计算方案数)
比赛时后面我一个多小时想这道题没有什么思路
主要是卡在怎么保证当前点一定选这件事情上,这也是关键
正解就是先保证当前点一定选,再考虑其他
想一下怎么使得当前点一定选,那么它一定要连比它权值大的边,它就一定会选
同时它不能连被选择的点,因为连了以后,被选择点就可以不选
所以为了保证它一定选,比它权值大且没有被选择的点数为cnt,那么这时方案数就是2的cnt次方-1
因为这些点选或不选为2的cnt次方,除去全都不选的情况。
那么它必选了之后就可以为所欲为了,对于那些权值比它小的点就可以随便选了。比它权值小的有n个,每个点选或不选,就是2的n次方
所以对于一个点它的方案数是 (2的cnt次方-1) * (2的n次方)
注意这里是乘法原理,分步完成用乘法
那么对于每一个被选择的点都求一个方案,同意是分步骤完成,所以这些方案全部都乘起来就是总方案数了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
const int mod = 1e9 + 7;
int f[N], n;
int add(int a, int b) { return (a + b) % mod; }
int mul(int a, int b) { return 1ll * a * b % mod; }
int main()
{
f[0] = 1;
_for(i, 1, 1e5) f[i] = mul(f[i - 1], 2);
string k;
while(cin >> n >> k)
{
int ans = 1, len = k.size(), pre = n - k.size();
REP(i, 0, len)
{
if(k[i] == '1') ans = mul(ans, mul(f[pre] - 1, f[len - i - 1]));
else pre++;
}
printf("%d\n", ans);
}
return 0;
}
P3372 【模板】线段树 1(分块)
挺暴力的,这是个根号n的算法
本质就是每次处理时对一个块统一处理来节省时间
缺点是复杂度比较大,根号n,优点是更为灵活,可以维护更多复杂的区间修改和查询
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
ll a[N], mark[N], sum[N];
int st[N], ed[N], bl[N], n, m, block;
void init()
{
block = sqrt(n); //块的个数
_for(i, 1, block)
{
st[i] = n / block * (i - 1) + 1;
ed[i] = n / block * i;
}
ed[block] = n;
_for(i, 1, block)
_for(j, st[i], ed[i])
bl[j] = i;
}
int main()
{
scanf("%d%d", &n, &m);
init();
_for(i, 1, n)
{
scanf("%lld", &a[i]);
sum[bl[i]] += a[i];
}
while(m--)
{
int op, x, y, k;
scanf("%d", &op);
if(op == 1)
{
scanf("%d%d%d", &x, &y, &k);
if(bl[x] == bl[y]) _for(i, x, y) a[i] += k, sum[bl[i]] += k;
else
{
_for(i, x, ed[bl[x]]) a[i] += k, sum[bl[i]] += k;
_for(i, st[bl[y]], y) a[i] += k, sum[bl[i]] += k;
_for(i, bl[x] + 1, bl[y] - 1) mark[i] += k, sum[i] += k * (ed[i] - st[i] + 1);
}
}
else
{
scanf("%d%d", &x, &y);
ll ans = 0;
if(bl[x] == bl[y]) _for(i, x, y) ans += a[i] + mark[bl[x]];
else
{
_for(i, x, ed[bl[x]]) ans += a[i] + mark[bl[x]];
_for(i, st[bl[y]], y) ans += a[i] + mark[bl[y]];
_for(i, bl[x] + 1, bl[y] - 1) ans += sum[i];
}
printf("%lld\n", ans);
}
}
return 0;
}
P3919 【模板】可持久化线段树 1(可持久化数组)
可持久化线段树,可以访问历史版本的线段树
每一个版本对应一个root[i]
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
int root[N << 5], s[N << 5], ls[N << 5], rs[N << 5];
int cnt, n, m;
void build(int& k, int l, int r)
{
k = ++cnt;
if(l == r)
{
scanf("%d", &s[k]);
return;
}
int m = l + r >> 1;
build(ls[k], l, m);
build(rs[k], m + 1, r);
}
void add(int& k, int pre, int l, int r, int x, int p)
{
k = ++cnt;
ls[k] = ls[pre]; rs[k] = rs[pre];
if(l == r)
{
s[k] = p;
return;
}
int m = l + r >> 1;
if(x <= m) add(ls[k], ls[pre], l, m, x, p);
else add(rs[k], rs[pre], m + 1, r, x, p);
}
int query(int& k, int pre, int l, int r, int x)
{
k = ++cnt;
ls[k] = ls[pre]; rs[k] = rs[pre];
if(l == r) return s[k] = s[pre];
int m = l + r >> 1;
if(x <= m) return query(ls[k], ls[pre], l, m, x);
else return query(rs[k], rs[pre], m + 1, r, x);
}
int main()
{
scanf("%d%d", &n, &m);
build(root[0], 1, n);
_for(i, 1, m)
{
int pre, op, x, y;
scanf("%d%d%d", &pre, &op, &x);
if(op == 1)
{
scanf("%d", &y);
add(root[i], root[pre], 1, n, x, y);
}
else printf("%d\n", query(root[i], root[pre], 1, n, x));
}
return 0;
}
周三 5.5 (五一集训)
HH的项链(主席树)
这是我做这道题的第三种做法了
最近做了很多区间询问的问题,发现莫队,树状数组,主席树,分块都是很好的工具
这道题主席树可以在线做,树状数组和莫队都需要离线
这题主要用了可持久化线段树,即访问各个版本的线段树
用1表示这个元素的值是否存在,如果有两个同样的元素,则保留后一个,前一个为0
所以区间内1的个数就是答案 怎么维护呢
我们从左往右扫,一直更新,如果有重复的就把前面对应位置减一
这就导致了有n个版本的线段树,每一个版本的线段树保存了右端点为i的情况
所以我们就用可持久化线段树
当询问l, r时,我们就访问第r颗线段树,然后求这颗线段树里面[l, r]中1的个数就好了
注意这里可能需要修改两次,所以我们需要多设置一个变量作为一个中间的根节点
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
int root[N], ls[N << 5], rs[N << 5], t[N << 5];
int last[N], n, m, cnt;
void add(int& k, int pre, int l, int r, int x, int p)
{
k = ++cnt;
ls[k] = ls[pre]; rs[k] = rs[pre]; t[k] = t[pre] + p;
if(l == r) return;
int m = l + r >> 1;
if(x <= m) add(ls[k], ls[pre], l, m, x, p);
else add(rs[k], rs[pre], m + 1, r, x, p);
}
int query(int k, int l, int r, int x)
{
if(l == r) return t[k];
int m = l + r >> 1;
if(x > m) return query(rs[k], m + 1, r, x);
else return query(ls[k], l, m, x) + t[rs[k]];
}
int main()
{
scanf("%d", &n);
_for(i, 1, n)
{
int x; scanf("%d", &x);
if(!last[x]) add(root[i], root[i - 1], 1, n, i, 1);
else
{
int t; //中间根节点
add(t, root[i - 1], 1, n, last[x], -1);
add(root[i], t, 1, n, i, 1);
}
last[x] = i;
}
scanf("%d", &m);
while(m--)
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", query(root[r], 1, n, l));
}
return 0;
}
HDU 6574(数学概率)
考试时没什么思路,我这种什么概率期望的题几乎没做过
最后就根据样例猜公式,猜了七八个最后竟然猜中了,最后几分钟AC的
我看题不仔细,我以为选的是实数,结果选的是整数,如果是整数那就很简单了
又看漏条件了……
我做的时候很乱,就是感觉四个端点到底该怎么确定
正解是这样,只考虑左区间的右端点和右区间的左端点,而不考虑剩下两个端点
然后此时只要右区间左端点大于左区间右端点就行了。
一共有n方种可能,其中符合条件的有 1 + 2 + ……(n - 1) = n(n - 1) / 2
然后计算一下可以得出结果是(n + 1) / (2 * n)
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int mod = 1e9 + 7;
int mul(int a, int b) { return 1ll * a * b % mod; }
int add(int a, int b) { return (a + b) % mod; }
int binpow(int a, int b)
{
int res = 1;
for(; b; b >>= 1)
{
if(b & 1) res = mul(res, a);
a = mul(a, a);
}
return res;
}
int inv(int x) { return binpow(x, mod - 2); }
int main()
{
int n;
while(~scanf("%d", &n))
{
int ans = mul(n + 1, inv(2 * n));
printf("%d\n", ans);
}
return 0;
}
HDU 6567(树形dp)
比赛时我不知道树的重心,自己想了一个做法,和网上各种题解都不一样
赛后看到很多题解都是树的重心……
我就是在树形dp的过程中,处理出每个节点到当前节点的距离,可以发现向下跑时,总距离减小的子树节点个数,增加了非子树节点个数
最后要手推一下答案,有一点一点复杂
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int dp[N], son[N], n;
ll sum1, cnt1, ans1, num1;
ll sum2, cnt2, ans2, num2;
vector<int> g[N];
void dfs(int u)
{
son[u] = 1;
for(auto v: g[u])
{
if(son[v]) continue;
dp[v] = dp[u] + 1;
dfs(v);
son[u] += son[v];
}
}
void cal(int u, int fa, ll now)
{
for(auto v: g[u])
{
if(v == fa) continue;
ll t = now - son[v] + (cnt1 - son[v]);
num1 += t;
ans1 = min(ans1, t);
cal(v, u, t);
}
}
void cal2(int u, int fa, ll now)
{
for(auto v: g[u])
{
if(v == fa) continue;
ll t = now - son[v] + (cnt2 - son[v]);
num2 += t;
ans2 = min(ans2, t);
cal2(v, u, t);
}
}
int main()
{
scanf("%d", &n);
_for(i, 1, n - 2)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1);
_for(i, 1, n)
if(son[i])
{
sum1 += dp[i];
cnt1++;
}
ans1 = num1 = sum1;
cal(1, 0, sum1);
num1 /= 2;
int st;
_for(i, 1, n)
if(!son[i])
{
st = i;
break;
}
_for(i, 1, n) son[i] = 0;
dfs(st);
_for(i, 1, n)
if(son[i])
{
sum2 += dp[i];
cnt2++;
}
ans2 = num2 = sum2;
cal2(st, 0, sum2);
num2 /= 2;
printf("%lld\n", num1 + num2 + cnt1 * cnt2 + cnt1 * ans2 + cnt2 * ans1);
return 0;
}
poj 1655(树的重心模板题)
今天比赛遇到了一道树的重心的题目,补一下这个知识点
对于一颗树中的节点,最大子树节点数最小的点为重心
重心可能有多个,有多个的话距离和相同
重心有一个性质,即各个点到重心的距离和是最小的
求树的重心很简单,dfs一遍,统计子树节点数以及当前节点之上的子树的节点数就行了
#include<cstdio>
#include<vector>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e4 + 10;
int dp[N], son[N], n;
vector<int> g[N];
void dfs(int u, int fa)
{
son[u] = 1; dp[u] = 0;
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(v == fa) continue;
dfs(v, u);
son[u] += son[v];
dp[u] = max(dp[u], son[v]);
}
dp[u] = max(dp[u], n - son[u]);
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) g[i].clear();
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
int mi = 1e9, id = 0;
_for(i, 1, n)
if(dp[i] < mi)
{
id = i;
mi = dp[i];
}
printf("%d %d\n", id, mi);
}
return 0;
}
HDU 6567(树的重心 + 逆向思维)
这题如果知道树的重心,做法就会简单很多,我比赛时硬推的那个做法思维量挺大
显然就是把两个重心相连,求两次树的重心就好了。
连了以后怎么统计方案呢,直接统计非常麻烦,我们反过来统计,统计每一条边对答案有多少贡献
一条边u到v的贡献就是子树v的个数乘以子树u的个数
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int son[N], n, root[3], cnt[2], mi;
long long ans;
vector<int> g[N];
void init(int u, int fa)
{
cnt[1]++;
for(auto v: g[u])
{
if(v == fa) continue;
init(v, u);
}
}
void dfs(int u, int fa, int op)
{
son[u] = 1;
int now = 0;
for(auto v: g[u])
{
if(v == fa) continue;
dfs(v, u, op);
son[u] += son[v];
now = max(now, son[v]);
}
now = max(now, cnt[op] - son[u]);
if(mi > now) mi = now, root[op] = u;
}
void sum(int u, int fa)
{
son[u] = 1;
for(auto v: g[u])
{
if(v == fa) continue;
sum(v, u);
son[u] += son[v];
ans += 1ll * son[v] * (n - son[v]);
}
}
int main()
{
scanf("%d", &n);
_for(i, 1, n - 2)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
init(1, 0); cnt[2] = n - cnt[1];
mi = 1e9; dfs(1, 0, 1);
_for(i, 1, n)
if(!son[i])
{
mi = 1e9;
dfs(i, 0, 2);
break;
}
g[root[1]].push_back(root[2]);
g[root[2]].push_back(root[1]);
sum(1, 0);
printf("%lld\n", ans);
return 0;
}
Count on a tree(主席树 + lca + 树上差分)
这道题加深了我对主席树的理解,还能这么做
之前的主席树是对区间进行操作,就是求前缀和的线段树,然后做差分得到需要的线段树
这道题就变成了树上差分,现在的前缀和变成了从根节点到当前节点的线段树
当求u到v的路径时,就是root[u] + root[v] - root[lca[u, v]] - root[fa[lca[u, v]]]
就是普通的前缀和差分变成了树上差分
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
const int M = 20;
int d[N], up[N][M], val[N], lsh[N], n, m, len, cnt;
int root[N], ls[N << 5], rs[N << 5], t[N << 5];
vector<int> g[N];
void add(int& k, int pre, int l, int r, int x)
{
k = ++cnt;
ls[k] = ls[pre]; rs[k] = rs[pre]; t[k] = t[pre] + 1;
if(l == r) return;
int m = l + r >> 1;
if(x <= m) add(ls[k], ls[pre], l, m, x);
else add(rs[k], rs[pre], m + 1, r, x);
}
void dfs(int u, int fa)
{
d[u] = d[fa] + 1; up[u][0] = fa;
REP(j, 1, M) up[u][j] = up[up[u][j - 1]][j - 1];
add(root[u], root[fa], 1, len, val[u]);
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(v == fa) continue;
dfs(v, u);
}
}
int lca(int u, int v)
{
if(d[u] < d[v]) swap(u, v);
for(int j = M - 1; j >= 0; j--)
if(d[up[u][j]] >= d[v])
u = up[u][j];
if(u == v) return u;
for(int j = M - 1; j >= 0; j--)
if(up[u][j] != up[v][j])
u = up[u][j], v = up[v][j];
return up[u][0];
}
int query(int k1, int k2, int k3, int k4, int l, int r, int k)
{
if(l == r) return l;
int x = t[ls[k1]] + t[ls[k2]] - t[ls[k3]] - t[ls[k4]];
int m = l + r >> 1;
if(x >= k) return query(ls[k1], ls[k2], ls[k3], ls[k4], l, m, k);
else return query(rs[k1], rs[k2], rs[k3], rs[k4], m + 1, r, k - x);
}
void init()
{
_for(i, 1, n) lsh[i] = val[i];
sort(lsh + 1, lsh + n + 1);
len = unique(lsh + 1, lsh + n + 1) - lsh - 1;
_for(i, 1, n) val[i] = lower_bound(lsh + 1, lsh + len + 1, val[i]) - lsh;
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%d", &val[i]);
init();
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
while(m--)
{
int u, v, k;
scanf("%d%d%d", &u, &v, &k);
printf("%d\n", lsh[query(root[u], root[v], root[lca(u, v)], root[up[lca(u, v)][0]], 1, len, k)]); //注意询问的时候是root,不是编号
}
return 0;
}
SP10707 COT2 - Count on a tree II(树上莫队 + 欧拉序)
做题时如果没有什么明显思路或者充分思考了就可以看题解了。把握好题前独立思考的时间,不要太多也不要太少
这道题就是用树上莫队做
首先求不同颜色的个数就是莫队裸题,难点在于如何把一条路径上的节点转化到区间上
这里用到了一个新知识点叫欧拉序
欧拉序和dfs序有点像,不同的是dfs序最后是 R[u] = cnt 欧拉序是R[u] = ++cnt
也就是回溯的时候也记录下来
刚搜到这个点加入序列,回溯时也加入序列
有什么用呢,它可以很巧妙的把路径上的点拍到区间上,dfs序是拍子树,欧拉序可以拍路径
要分类讨论,对于点x,y,设st[x] < st[y]即x先访问到
(1)lca(x, y) == x
此时在一条链上,取序列中st[x]到st[y]
我们发现中间有重复的节点,这些节点都不是链上的点,因为他们半路出去了又回来了,加入了两次
出现了一次的就是链上的点
所以这个序列中出现了一次的点就是链上的点
(2) lca(x, y) != x
这个时候x和y在不同子树,我们看ed[x]到st[y]这个区间
可以想象那个过程,就是从x回溯然后搜到y
同样可以发现,有些点出现两次,那就是跑到其他子树的点进去又出来了,所以同样就是这个区间内出现一次的点就是路径上的点
然后注意可以发现这时lca没有出现在序列中,所以到时候处理的时候要特判一下
一些细节写在注释里面了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
const int M = 20;
int d[N], up[N][M], val[N], a[N], lsh[N], pos[N << 1], n, m, len, cnt;
int st[N << 1], ed[N << 1], use[N], ans[N], num[N], sum;
vector<int> g[N];
struct query
{
int l, r, bl, id, lca;
}q[N];
void dfs(int u, int fa)
{
st[u] = ++cnt; pos[cnt] = u; //细节,要记录序列中对应的原来节点的编号
d[u] = d[fa] + 1; up[u][0] = fa;
REP(j, 1, M) up[u][j] = up[up[u][j - 1]][j - 1];
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(v == fa) continue;
dfs(v, u);
}
ed[u] = ++cnt; pos[cnt] = u; //欧拉序
}
int lca(int u, int v)
{
if(d[u] < d[v]) swap(u, v);
for(int j = M - 1; j >= 0; j--)
if(d[up[u][j]] >= d[v])
u = up[u][j];
if(u == v) return u;
for(int j = M - 1; j >= 0; j--)
if(up[u][j] != up[v][j])
u = up[u][j], v = up[v][j];
return up[u][0];
}
void init()
{
_for(i, 1, n) lsh[i] = val[i];
sort(lsh + 1, lsh + n + 1);
len = unique(lsh + 1, lsh + n + 1) - lsh - 1;
_for(i, 1, n) val[i] = lower_bound(lsh + 1, lsh + len + 1, val[i]) - lsh;
}
bool cmp(query a, query b)
{
if(a.bl != b.bl) return a.bl < b.bl;
if(a.bl & 1) return a.r < b.r;
return a.r > b.r;
}
void add(int x) { sum += ++num[x] == 1; }
void del(int x) { sum -= --num[x] == 0; }
void Add(int x)
{
use[x] ? del(val[x]) : add(val[x]); //判断点的编号,出现了两次就删除
use[x] ^= 1;
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%d", &val[i]);
init();
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
int block = sqrt(2 * n); //注意这时总长度乘以2 使用欧拉序的时候尤其注意
_for(i, 1, m)
{
int x, y;
scanf("%d%d", &x, &y);
if(st[x] > st[y]) swap(x, y);
int t = lca(x, y);
if(t == x) q[i] = {st[x], st[y], st[x] / block, i, 0};
else q[i] = {ed[x], st[y], ed[x] / block, i, t};
}
sort(q + 1, q + m + 1, cmp);
int l = 1, r = 0;
_for(i, 1, m)
{
int ll = q[i].l, rr = q[i].r;
while(l < ll) Add(pos[l++]); //这这个点的出现次数发生变化,不知道是增加还是删除,用Add函数判断
while(l > ll) Add(pos[--l]); //注意区分序列中的编号和节点的编号
while(r < rr) Add(pos[++r]);
while(r > rr) Add(pos[r--]);
if(q[i].lca) Add(q[i].lca); //特判lca
ans[q[i].id] = sum;
if(q[i].lca) Add(q[i].lca); //注意这一句话,要把之前lca的贡献删掉。因为这里不删后面是不会删的,因为序列中没有
} //每次单独判断,改变次数,统计答案,再改变次数
_for(i, 1, m) printf("%d\n", ans[i]);
return 0;
}
HDU 4417 (主席树)
主席树就好辽
首先要离散化,把题目给的高度和询问的高度弄在一起离散化
建立权值线段树,每次查询就查找权值[1, max_h]有多少个数就好了
lsh数组可以用一个num,写起来方便一点。注意初始化num
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e5 + 10;
int t[N << 5], ls[N << 5], rs[N << 5];
int h[N], lsh[N], root[N], num, n, m, len, cnt;
struct query
{
int l, r, max_h;
}q[N];
void add(int& k, int pre, int l, int r, int x)
{
k = ++cnt;
ls[k] = ls[pre]; rs[k] = rs[pre]; t[k] = t[pre] + 1;
if(l == r) return;
int m = l + r >> 1;
if(x <= m) add(ls[k], ls[pre], l, m, x);
else add(rs[k], rs[pre], m + 1, r, x);
}
int query(int k, int pre, int l, int r, int h)
{
if(l == r) return t[k] - t[pre];
int m = l + r >> 1;
if(h <= m) return query(ls[k], ls[pre], l, m, h);
else return query(rs[k], rs[pre], m + 1, r, h) + t[ls[k]] - t[ls[pre]];
}
void init()
{
sort(lsh + 1, lsh + num + 1);
len = unique(lsh + 1, lsh + num + 1) - lsh - 1;
_for(i, 1, n) h[i] = lower_bound(lsh + 1, lsh + len + 1, h[i]) - lsh;
_for(i, 1, m) q[i].max_h = lower_bound(lsh + 1, lsh + len + 1, q[i].max_h) - lsh;
}
int main()
{
int T, kase = 0; scanf("%d", &T);
while(T--)
{
cnt = num = 0;
memset(t, 0, sizeof(t));
scanf("%d%d", &n, &m);
_for(i, 1, n)
{
scanf("%d", &h[i]);
lsh[++num] = h[i];
}
_for(i, 1, m)
{
scanf("%d%d%d", &q[i].l, &q[i].r, &q[i].max_h);
q[i].l++; q[i].r++;
lsh[++num] = q[i].max_h;
}
init();
_for(i, 1, n) add(root[i], root[i - 1], 1, len, h[i]);
printf("Case %d:\n", ++kase);
_for(i, 1, m) printf("%d\n", query(root[q[i].r], root[q[i].l - 1], 1, len, q[i].max_h));
}
return 0;
}
周五 5.7(主席树)
昨天复习考试去了
P2617 Dynamic Rankings(支持单点修改的主席树)
之前做的主席树都是静态的,不支持修改的,现在这道题要求支持单点修改求区间第k大
如果按照原来的想法
root[i]表示1到i这个前缀线段树
那么修改a[i] 那么root[i……n]的线段树都要修改
那么一次修改的时间复杂度变成nlogn了,肯定超时
考虑怎么优化
我们要统计答案的话,是两个前缀和相减
前缀和,单点修改
用树状数组维护就很方便了
所以以前root[i]表示1到i,现在root[i] 表示树状数组中的c[i]
那么现在修改一次,就要修改logn个线段树
同理,统计前缀和也是要统计logn个线段树
由于是树状数组套线段树,多了个线段树
所以时间复杂度和空间复杂度都要多一个log
所以一次修改的时间复杂度是logn的平方,同时多开了logn的平方个节点
所以注意空间大小是nlognlogn 在这道题就要乘个400,我因为这个WA了好多次
还有一些点
之前写主席树都会复制前面节点的,这次不会
因为之前主席树是root[i] = root[i - 1] + a[i]
是要前面的root[i - 1]的,所以需要复制
而这次树状数组,平时写梳妆数组不就直接f[x]++
所以不需要复制,直接修改就好
同时新开点的前提时不存在,因为树状数组是会重复遍历一个根节点的
然后区分权值的最大值和下标的最大值,我一开始搞错了
树状数组是下标最大值,主席树中是权值最大值
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int root[N << 1], ls[N * 400], rs[N * 400], t[N * 400];
int a[N], lsh[N << 1], len, cnt, n, m;
int now[20], pre[20], cntn, cntp;
struct query
{
int op, l, r, k;
}q[N];
int lowbit(int x) { return x & (-x); }
void init()
{
sort(lsh + 1, lsh + len + 1); //离散化始终用一个len 方便不容易出错
len = unique(lsh + 1, lsh + len + 1) - lsh - 1;
_for(i, 1, n) a[i] = lower_bound(lsh + 1, lsh + len + 1, a[i]) - lsh;
_for(i, 1, m) if(q[i].op == 2) q[i].r = lower_bound(lsh + 1, lsh + len + 1, q[i].r) - lsh;
}
void add(int& k, int l, int r, int x, int p)
{
if(!k) k = ++cnt; //注意这里会重复遍历到,要判断一下。以前不用判断是因为一定是新建的。写了更通用
t[k] += p; //不像之前一样复制理解,深刻理解
if(l == r) return; //add记得return
int m = l + r >> 1;
if(x <= m) add(ls[k], l, m, x, p);
else add(rs[k], m + 1, r, x, p);
}
void Add(int i, int p) //添加第i个位置的数
{
for(int x = i; x <= n; x += lowbit(x)) //len表示值域,而n表示下标,要区分开
add(root[x], 1, len, a[i], p);
}
int query(int l, int r, int k)
{
if(l == r) return l;
int x = 0;
_for(i, 1, cntn) x += t[ls[now[i]]];
_for(i, 1, cntp) x -= t[ls[pre[i]]];
int m = l + r >> 1;
if(x >= k)
{
_for(i, 1, cntn) now[i] = ls[now[i]];
_for(i, 1, cntp) pre[i] = ls[pre[i]];
return query(l, m, k);
}
else
{
_for(i, 1, cntn) now[i] = rs[now[i]];
_for(i, 1, cntp) pre[i] = rs[pre[i]];
return query(m + 1, r, k - x);
}
}
int Query(int l, int r, int k)
{
cntn = cntp = 0;
for(int x = r; x; x -= lowbit(x)) now[++cntn] = root[x]; //
for(int x = l - 1; x; x -= lowbit(x)) pre[++cntp] = root[x]; //
return query(1, len, k);
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%d", &a[i]), lsh[++len] = a[i];
_for(i, 1, m)
{
char op[5];
int l, r, k;
scanf("%s%d%d", op, &l, &r);
if(op[0] == 'Q')
{
scanf("%d", &k);
q[i] = {1, l, r, k};
}
else q[i] = {2, l, r}, lsh[++len] = r;
}
init();
_for(i, 1, n) Add(i, 1);
_for(i, 1, m)
{
if(q[i].op == 2)
{
int x = q[i].l, y = q[i].r; //将a[x]改为y
Add(x, -1);
a[x] = y;
Add(x, 1);
}
else printf("%d\n", lsh[Query(q[i].l, q[i].r, q[i].k)]);
}
return 0;
}
HDU 2089(数位dp入门题)
主席树告一段落
现在开始刷各种类型的dp
先是数位dp
https://www.luogu.com.cn/blog/virus2017/shuweidp
这篇博客写得很好
数位dp是有模板的,按照题目进行修改即可
很套路
讲几个关键点
一.数位dp用来解决一个区间内有多少个符合某种性质的数的问题
二.一般用记忆化搜索实现,dp状态尽可能多
三.注意最高位限制和前导0,用dp状态时前提是没有最高位限制和前导0
四.开始的区间注意起点为0的情况
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
ll a[30], dp[15][15], len;
ll dfs(int pos, int pre, int lead, int limit)
{
if(pos > len) return 1; //找到答案返回
if(dp[pos][pre] != -1 && !lead && !limit) return dp[pos][pre]; //记忆化 前提是非lead和limit
ll res = 0;
int mx = limit ? a[len - pos + 1] : 9; //mx为当前位能取的最高位
_for(i, 0, mx)
{
if(i == 4) continue;
if(!i && lead) res += dfs(pos + 1, i, 1, i == mx && limit); //当前位为0且为前导0.只有这种情况lead依然为1
else if(!(i == 2 && pre == 6)) res += dfs(pos + 1, i, 0, i == mx && limit); //limit的传递是固定的
}
if(!lead && !limit) dp[pos][pre] = res;
return res;
}
ll work(ll x)
{
len = 0;
while(x) a[++len] = x % 10, x /= 10;
memset(dp, -1, sizeof(dp)); //记忆化搜索-1好一些 有时候dp的答案就是0
return dfs(1, 0, 1, 1);
}
int main()
{
ll l, r;
while(scanf("%lld%lld", &l, &r) && l)
printf("%lld\n", work(r) - (!l ? 0 : work(l - 1))); //注意l为0的情况
return 0;
}
周六 5.8(数位dp)
P2657 [SCOI2009] windy 数
套模板
注意一些细节,写在注释了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
ll len, a[35], dp[35][15];
ll dfs(int pos, int pre, int lead, int limit)
{
if(pos > len) return 1;
if(dp[pos][pre] != -1 && !lead && !limit) return dp[pos][pre];
ll res = 0, mx = limit ? a[len - pos + 1] : 9;
_for(i, 0, mx)
{
if(!i && lead) res += dfs(pos + 1, 0, 1, i == mx && limit); //有前导0且自己为0,则lead=1传下去
else if(i && lead) res += dfs(pos + 1, i, 0, i == mx && limit); //有前导0但是自己不是0,即这是最高位。这时不用判前后差
else if(abs(i - pre) >= 2) res += dfs(pos + 1, i, 0, i == mx && limit); //一般情况
}
if(!lead && !limit) dp[pos][pre] = res;
return res;
}
ll work(ll x)
{
len = 0;
memset(dp, -1, sizeof(dp)); //一开始要初始化
while(x) a[++len] = x % 10, x /= 10;
return dfs(1, 0, 1, 1); //一开始看作有前导0 故pre为0
}
int main()
{
ll l, r;
scanf("%lld%lld", &l, &r);
printf("%lld\n", work(r) - work(l - 1));
return 0;
}
P2602 [ZJOI2010]数字计数
数位dp就是注意一些细节
设计dp状态时要尽可能多,比如这道题要把当前的答案也加入状态
否则就会出错
看dfs的参数,把尽可能多的参数加入状态才不会出错
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
ll ans[10], a[50], dp[50][15], len;
ll dfs(int pos, ll sum, int x, int lead, int limit)
{
if(pos > len) return sum;
if(dp[pos][sum] != -1 && !lead && !limit) return dp[pos][sum];
ll res = 0, mx = limit ? a[len - pos + 1] : 9;
_for(i, 0, mx)
{
if(!i && lead) res += dfs(pos + 1, sum, x, 1, i == mx && limit);
else res += dfs(pos + 1, sum + (i == x), x, 0, i == mx && limit);
}
if(!limit && !lead) dp[pos][sum] = res;
return res;
}
void work(ll x, int p)
{
len = 0;
memset(dp, -1, sizeof(dp));
while(x) a[++len] = x % 10, x /= 10;
_for(i, 0, 9)
ans[i] += p * dfs(1, 0, i, 1, 1);
}
int main()
{
ll l, r;
scanf("%lld%lld", &l, &r);
work(r, 1); work(l - 1, -1);
_for(i, 0, 9) printf("%lld ", ans[i]); puts("");
return 0;
}
P3413 SAC#1 - 萌数
这题要注意一些地方
(1)这道题n的位数很大,所以要用字符串。l-1用高精度减1不好处理,可以直接特判l
(2)这道题是符合一个条件这个数就符合
符合了之后,还有包括后面的各种取值情况,所以对答案的贡献是很大的
我一开始想的是推一下公式算出来,发现还要分是否为最高位,不太好推
后来意识到继续往后跑就好了,dfs中多设置一个参数代表是否为答案, 最后直接范围这个参数就行
注意这时候dp状态就要加上答案这个参数
dp状态的参数看dfs的参数,最好除了lead 和limit外其他参数全部加上
尤其要加上当前这个数是否ok这个状态,我一开始漏掉了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int N = 1e3 + 10;
ll dp[N][15][15][2], a[N], len, ans;
ll add(ll a, ll b) { return (a + b) % mod; }
ll mul(ll a, ll b) { return 1ll * a * b % mod; }
ll dfs(int pos, int pre1, int pre2, int ok, int lead, int limit)
{
if(pos > len) return ok;
if(dp[pos][pre1][pre2][ok] != -1 && !lead && !limit) return dp[pos][pre1][pre2][ok];
ll res = 0, mx = limit ? a[pos] : 9;
_for(i, 0, mx)
{
if(!i && lead) res = add(res, dfs(pos + 1, 10, 10, ok, 1, i == mx && limit));
else if(i && lead) res = add(res, dfs(pos + 1, i, 10, ok, 0, i == mx && limit));
else res = add(res, dfs(pos + 1, i, pre1, ok | (pre1 == i || pre2 == i), 0, i == mx && limit));
}
if(!limit && !lead) dp[pos][pre1][pre2][ok] = res;
return res;
}
bool check(string s)
{
int n = s.size();
REP(i, 0, n)
if(i >= 1 && (s[i] == s[i - 1]) || i >= 2 && (s[i] == s[i - 2]))
return true;
return false;
}
ll work(string s)
{
len = 0; memset(dp, -1, sizeof(dp));
int n = s.size();
REP(i, 0, n) a[++len] = s[i] - '0';
return dfs(1, 10, 10, 0, 1, 1);
}
int main()
{
string l, r;
cin >> l >> r;
ll ans = work(r) - work(l) + check(l);
printf("%lld\n", (ans + mod) % mod);
return 0;
}
P4127 [AHOI2009]同类分布
这道题我是看题解的
主要是不知道这么处理那么大的数,不知道这么用记忆化来加速
因为结果要求now % sum == 0
从这里出发,把所有数看作模sum
储存原数太大了,炸空间
所以就取模,而答案又刚好需要取模,所以我们就取模就好了
同余看作相同的,太秀了
但是又一个问题就是不搜到最后我们不知道这个模数是多少
那就用一个非常暴力的方法,枚举每一个模数,只有最后数字和刚好等于这个模数的时候才统计答案
太秀了
另外这道题的前导0是不影响的,所以不用记录,代码就短了一些
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
ll a[20], dp[20][200][200], len;
ll dfs(int pos, int sum, ll now, int p, int limit)
{
if(pos > len) return sum == p && now == 0;
if(dp[pos][sum][now] != -1 && !limit) return dp[pos][sum][now];
ll res = 0, mx = limit ? a[len - pos + 1] : 9;
_for(i, 0, mx)
res += dfs(pos + 1, sum + i, (now * 10 + i) % p, p, i == mx && limit);
if(!limit) dp[pos][sum][now] = res; //记忆化不要忘记
return res;
}
ll work(ll x)
{
len = 0;
while(x) a[++len] = x % 10, x /= 10;
ll res = 0;
_for(i, 1, len * 9)
{
memset(dp, -1, sizeof(dp));
res += dfs(1, 0, 0, i, 1);
}
return res;
}
int main()
{
ll l, r;
scanf("%lld%lld", &l, &r);
printf("%lld\n", work(r) - work(l - 1));
return 0;
}
发现以前效率挺低的
我的训练效率还可以再提高
我以前会一道题卡很久很久,就死磕,其实很多时候是不必要的
既不要马上看题解,也不要长时间死磕
平衡好,提高效率,在保证质量的前提下尽可能做多的题目
周日 5.9(数位dp)
P4317 花神的数论题
这道题想了挺久最后是60分,后面几个大数据的点不知道为什么WA了
看了题解发现我想复杂了
(1)首先这道题有无前导0没有关系,所以不用管
(2)这道题的模数不是1e9 + 7 以后直接复制
(3)我自己想的时候想推公式,弄得很复杂
实际上不用推,用程序帮你算就好,一直往下跑,记忆化使得不会超时的
这道题就是一直往下跑推出一个答案,然后记忆化使得加速
(4)这道题的一个变化是求乘积,之前都是求和
这个地方我也写复杂了,实际上只需要统计答案时把加改成乘法就好了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int mod = 10000007;
const int N = 100;
ll dp[N][N], a[N], len;
ll dfs(int pos, int sum, int limit)
{
if(pos > len) return max(sum, 1);
if(dp[pos][sum] != -1 && !limit) return dp[pos][sum];
ll res = 1, mx = limit ? a[len - pos + 1] : 1;
_for(i, 0, mx) res = res * dfs(pos + 1, sum + i, i == mx && limit) % mod;
if(!limit) dp[pos][sum] = res;
return res;
}
int main()
{
ll x; scanf("%lld", &x);
while(x) a[++len] = x & 1, x >>= 1;
memset(dp, -1, sizeof(dp));
printf("%lld\n", dfs(1, 0, 1));
return 0;
}
「一本通 5.3 练习 1」数字游戏
这道题就是那个数字计数的弱化版
核心就是把大数取模,才不会炸空间,也能够记忆化
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 100;
ll dp[N][N], a[N], len, p;
ll dfs(int pos, int sum, int limit)
{
if(pos > len) return sum == 0;
if(dp[pos][sum] != -1 && !limit) return dp[pos][sum];
ll res = 0, mx = limit ? a[len - pos + 1] : 9;
_for(i, 0, mx) res += dfs(pos + 1, (sum + i) % p, i == mx && limit);
if(!limit) dp[pos][sum] = res;
return res;
}
ll cal(ll x)
{
len = 0;
while(x) a[++len] = x % 10, x /= 10;
memset(dp, -1, sizeof(dp));
return dfs(1, 0, 1);
}
int main()
{
ll l, r;
while(~scanf("%lld%lld%lld", &l, &r, &p))
printf("%lld\n", cal(r) - cal(l - 1));
return 0;
}
「一本通 5.3 例 2」数字游戏
水题
与前导0无关,枚举位数的时候从前一位开始枚举就好
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
ll dp[100][10], a[100], len, p;
ll dfs(int pos, int pre, int limit) //sum1数字和 sum2是数本身
{
if(pos > len) return 1;
if(dp[pos][pre] != -1 && !limit) return dp[pos][pre];
ll res = 0, mx = limit ? a[len - pos + 1] : 9;
_for(i, pre, mx) res += dfs(pos + 1, i, i == mx && limit);
if(!limit) dp[pos][pre] = res;
return res;
}
ll cal(ll x)
{
len = 0;
while(x) a[++len] = x % 10, x /= 10;
memset(dp, -1, sizeof(dp));
return dfs(1, 0, 1);
}
int main()
{
ll l, r;
while(~scanf("%lld%lld", &l, &r))
printf("%lld\n", cal(r) - cal(l - 1));
return 0;
}
把几道简单的题刷掉了