大一下第十周学习笔记

周一 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;
}

把几道简单的题刷掉了

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值