数据结构

P2168 荷马史诗

总长度最小

huffman树,就是合并果子用的那个数据结构(应该算数据结构),指的是带权路径和最短的一棵树。

(看题解说这是一道K叉Huffman树的板子题,但是我浅浅看了一下,好像除了这题只做过一道合并果子树的二叉Huffman树)

二叉哈夫曼树就是每次选取权值最小的两个数进行合并。这一题是k叉Huffman树,在题解中的应用lyd大佬的话:

对于k叉哈夫曼树的求解,直观的想法是在贪心的基础上,改为每次从堆中去除最小的k个权值合并。然而,仔细思考可以发现,如果在执行最后一次循环时,堆的大小在(2~k-1)之间(不足以取出k个),那么整个哈夫曼树的根的子节点个数就小于k。这显然不是最优解————我们任意取哈夫曼树中一个深度最大的节点,改为树根的子节点,就会使 变小。”

所以在开始建树之前要先判断一下 \((n-1)\)%\((k-1)\)是不是为 \(0\) ,如果没有办法整除的话就补充一些权值为 \(0\) 的叶子节点

将建的这棵\(Huffman\)树看做一棵\(Trie\)树,也就是说每一个节点的对应编码值就是从根节点到那个节点所经过的路径,深度对应的即是单词的长度。

不是前缀

题目中的任意一个单词对应编码都不是其余单词的前缀,即在\(Trie\)树中每一个单词对应的编码的结尾都是叶子节点。

最长的字母长度最短

最长的字母长度最短,再结合上面的,也就是说树的深度要尽量小一点,所以在用优先队列的时候,重载一下小于号

注意这题要开long long,千万不要开一半然后剩下一半没有开!!!

核心代码

const int N=1e5+5,inf=0x3f3f3f3f;

struct node{
	lwl v,h;
	bool operator < (const node &it) const{
		if(v == it.v) return h > it.h;
		return v > it.v;
	}
};

void huffman(){
	while ((n - 1) % (m - 1)){
		n ++;
		q.push({0, 1});
	}
	
	while(q.size() > 1){
		lwl sum = 0,dep = 0;
		
		for (int i = 1; i <= m; i ++){
			auto t = q.top();
			q.pop();
			lwl v = t.v, h = t.h;
			ans += v;
			sum += v;
			dep = max(dep, h);
		}
		q.push({sum, dep + 1});
		maxn = max(dep + 1, maxn);
	}
}

括号画家

ACwing上面的题,题号150

题目描述:

这一天,刚刚起床的达达画了一排括号序列,其中包含小括号 ( )、中括号 [ ] 和大括号 { },总长度为 \(N\)

这排随意绘制的括号序列显得杂乱无章,于是达达定义了什么样的括号序列是美观的:

空的括号序列是美观的;

若括号序列\(A\)是美观的,则括号序列 (\(A\))、[\(A\)]、{\(A\)} 也是美观的;

若括号序列 \(A\)\(B\)都是美观的,则括号序列 \(AB\) 也是美观的。

例如 ( ) { } ( ) 是美观的括号序列,而) ( { ) } ( 则不是。

现在达达想在她绘制的括号序列中,找出其中连续的一段,满足这段子串是美观的,并且长度尽量大。

这题很明显要用栈,但是我一开始做的时候想复杂了,所以搞了三个栈存三种,然后做的时候发现好像这样只能处理前一个括号多了的情况,但是没有办法判断前一个括号少了的情况(可能可以判断,但感觉做起来会比较麻烦,所以就没有写)于是乎把代码都注释掉,看看其他方法。

然后看到不是在线处理,于是就把他弄到一个栈里面去,然后在判断的时候,就只看当前这个是不是大括号,如果是大括号的话就把前面对应的一个小括号给删除掉。并且显而易见,如果这两个括号是对应的并且他可以形成一个美观的子串,那说明这两个中间的括号都是已经被弹出去了的,所以此时只用判断栈顶的元素是不是对应的前括号,如果是的话就弹出,如果不是的话就把这个放到栈里,其实就相当于把这个点当做一个新的断点,之后不管怎么样到这里就停住了

核心代码

char t = str[i];
if (t == ')' && s.size() && str[s.top()] == '(')
	s.pop();
	else if (t == ']' && s.size() && str[s.top()] == '[')
		s.pop();
	else if (t == '}' && s.size() && str[s.top()] == '{')
		s.pop();
	else s.push(i);
	if(s.size()) ans = max(ans, i - s.top());
	else ans = i + 1;

表达式求值 4

也是\(ACwing\)上面的,好像和数据结构没有什么大关系,应该算是一个大模拟吧。但是这个模拟确实是好难写,烦死了。

就是写的时候注意一下每次取栈顶的时候要判断一下栈是不是空的,要不然就会RE很多很多次(),然后\(ACwing\)上面最后一个数据是

(-1)))

所以说当读到后括号的时候,不要着急去弹了,也要判断一下栈是不是空的,不然也会\(RE\),在那个计算的时候我本来打算的是把num的size在进入计算之前判断一下是不是大于2的,但是发现它不大于2的时候也是要弹符号的,所以干脆就加在cal()里面了。

还有就是,这个表达式计算的时候,要注意先取出来的是在后面的,意思就是如果先是\(a\)被弹出来,\(b\)后弹出来,那么计算乘方除法和减法的时候,要计算的就是\(b/a\)\(b-a\)\(b^a\)

大模拟代码就不贴了,太长了看着有点烦()

P1487 玉蟾宫

这一题在\(ACwing\)上面叫城市游戏,不过不重要

这一题可以转化成直方图中的最大矩阵,就是一列一列的来看,然后每个点的\(w\)值我存的就是:

如果这个点是\(F\),那么就是这个点一直往上走直到那个点不是\(F\)的时候,对应的值,其实就是说这个点上面有多少个连续的\(F\)

如果是\(S\)的话,那么就直接赋值\(0\)就可以了

然后就相当于对于每一行都求一个最大矩阵就可以啦!最大矩阵用单调栈就可以了,数组模拟单调栈应该可以快不少,但是我是\(STL\)忠实爱好者,宁愿写快读快写、手写min和max都不想数组模拟,于是乎用了\(STL\)的stack,小问题。

核心代码

int work(int h[]){
	h[0] = h[m + 1] = -1;
	
	stack<int> s;
	for (int i = 1; i <= m; i ++){
		while (s.size() && h[s.top()] >= h[i])
			s.pop();
		if (s.size()) l[i] = s.top();
		else l[i] = 0;
		s.push(i);
	}
	
	stack<int> q;
	for (int i = m; i; i --){
		while (q.size() && h[q.top()] >= h[i])
			q.pop();
		if (q.size()) r[i] = q.top();
		else r[i] = m + 1;
		q.push(i);
	}
	
	int ans = 0;
	for (int i = 1; i <= m; i ++)
		ans = max(ans, h[i] * (r[i] - l[i] - 1));
	
	return ans;
}

//int main()里面的
for (int i = 1; i <= n; i ++){
		for (int j = 1; j <= m; j ++){
			cin>>s;
			if (s == "F")
				w[i][j] = w[i - 1][j] + 1;
		}
	}

P1155 双栈排序

是一道好难写的题目()

因为一开始看到想了二十多分钟没有思路于是乎果断打开题解看,因为一开始是在\(ACwing\)上面做的,所以就看的是y总代码,看懂了,好像也没有什么问题,于是乎照着写了一遍,但是交上去错了。一开始确实是二分图那个地方写错了,但是后来改完之后再看就找不到错误了,觉得可能是二分图那里有点小问题,于是把y总代码复制下来想看看正确的染色是什么样子,结果发现y总代码也是错的,难蚌

这一题主要是用两个栈(算模拟吧())和二分图,那个二分图我确实是没有想到。

这题有一个很强的性质:

两个数\(i\)\(j\)\(i\) \(≤\) \(j\))不能放在同一个栈里当且仅当存在一个数\(k\)\(k\) \(>\) \(j\),且\(w\)[ \(k\) ] \(<\) \(w\)[ \(i\) ] \(<\) \(w\)[ \(j\) ]

证明:

必要性:

如果有 \(i\) \(<\) \(j\) \(<\) \(k\),且\(w\)[ \(k\) ] \(<\) \(w\)[ \(i\) ] \(<\) \(w\)[ \(j\) ],则因为 \(w\)[ \(i\) ] 和\(w\)[ \(k\) ]的后面均存在一个更小的数\(w\)[ \(k\) ],因此\(w\)[ \(i\) ]和\(w\)[ \(j\) ]均不能从栈中被弹出,所以栈底的到栈顶的元素就不是单调的降序了,那么弹出的时候得到的序列就会出现逆序对。

充分性:

如果 \(i\)\(j\) 不满足上述条件,那么将他们放在一个栈里一定不会出现矛盾。

在操作过程中,如果当前要入栈的数是 \(w\)[ \(j\) ],那么此时:

  • 所有大于\(w\)[ \(j\) ]的元素,都一定在栈中
  • 所有小于\(w\)[ \(j\) ]的元素,比如\(w\)[ \(i\) ] \(<\) \(w\)[ \(j\) ],则由于后面不存在\(w\)[ \(k\) ] \(<\) \(w\)[ \(i\) ],因此此时\(w\)[ \(i\) ]一定已经出栈

有了上面的性质,就可以把所有有这种性质的两个数连一条边,然后用染色法。因为题目要求字典序最大,所以优先对于第一个栈进行操作。

二分染色法的代码就不贴了,还有错误代码贴在飞书上面了。

核心代码

//建边
f[n + 1] = n + 1;
for (int i = n; i; i --) {
	f[i] = min(f[i + 1],w[i]);
}
	
for (int i = 1; i <= n; i ++) {
	for (int j = i + 1; j <= n; j ++)
		if(w[i] < w[j] && f[j + 1] < w[i])
			flag[i][j] = flag[j][i] = true;
}

//对于栈操作
if (j <= n && color[j] == 0 && (!s1.size()||s1.top() > w[j])){
	s1.push(w[j]);
	j ++;
	printf("a ");
} else if (s1.size() && s1.top() == u){
	s1.pop();
	printf("b ");
	u ++;
} else if (j <= n &&color[j]&& (!s2.size()||s2.top() > w[j])){
	s2.push(w[j]);
	j ++;
	printf("c ");
} else if (s2.size() && s2.top() == u){
	s2.pop();
	printf("d ");
	u ++;
}

P5763 内存分配

这题好像就是一个纯粹的大模拟,但是又不是那么不动脑子的模拟

写两个小函数,一个是插入程序:

  • 一个是到了一个时间点看看能不能完成程序释放内存,并且在释放内存后看看能不能从等待队列前面找几个程序插进去。
  • 还有一个就是插入的具体过程

然后就是要注意的几个点(其实就是一开始写的时候写错的地方)

  1. 在进while之前,要先在\(-1\)\(n\)的地方加一个\(t\)=\(1\)的程序,也就是让他们有一个范围,不然就会把所有程序都甩到等待队列里面,然后ans输出0

  2. 在while结束之后,要记得finish一个极大值,不然最后的程序就跑不完了()

然后一些核心代码

bool insert(int tim,int m,int p){
    for(auto u = yx.begin(); u != yx.end(); u ++){
        auto v = u;
        v ++
        if (v != yx.end()){
            if (m <= v->fi - (u->fi + u->se - 1) - 1){
                int be = u->fi + u->se;
                yx.insert({be,m});
                q.push({tim + p, be});
                return true;
            }
        }
    }
    
    return false;
}


void finish(int tim){
    while (q.size() && q.top().fi <= tim){
        int h = q.top().fi;
        while (q.size() && q.top().fi == h){
            auto t = q.top();
            q.pop();
            
            auto it = yx.lower_bound({t.se,0});
            yx.erase(it);
        }
        
        ans = h;
        while (dd.size()){
            auto t = dd.front();
            if (insert(h,t.fi,t.se))
                dd.pop();
            else break;
        }
    }
}


while (1){
	int tim = fr(), m = fr(), p = fr();
	if (!tim && !m && !p) break;
	finish(tim);
	if (!insert(tim,m,p)){
		dd.push({m,p});
		cnt ++;
	}
}

矩阵

ACwing上面的题,题号156

给定一个 \(M\)\(N\) 列的 \(01\) 矩阵(只包含数字 \(0\)\(1\) 的矩阵),再执行 \(Q\) 次询问,每次询问给出一个 \(A\)\(B\) 列的 \(01\) 矩阵,求该矩阵是否在原矩阵中出现过。

这是一道字符串哈希,把这个矩阵按照哈希字符串的方式给存起来,然后再读入每一个矩阵,把每一个矩阵也转化为字符串哈希的方式给存下来,然后把它们都加到set和队列里面。

然后暴力遍历原图中的每一个点,把它们所对应的矩阵哈希从set里面擦除掉。看题解说不擦除而是都存起来的话,那set里面会用很多空间,所以就擦除,如果set里面有的话就可以标记一下

然后再遍历一遍队列,也就是从头到尾遍历一下询问的矩阵,如果当前队列所对应的哈希值已经被擦除掉了,就说明在前面遍历每一个点的时候有过这个矩阵,就直接输出1,不然就输出0

看起来还是挺好写的,但是我写的时候错了好几次,但是因为都是样例错的所以不太记得具体是错在哪里了。还记得几个()

注意事项

  1. 开数组 \(p\) 的时候不要用 \(N\) 了,要用 \(N^2\) ,不然就开小了
  2. 在初始化 \(p\) 数组的时候,切记不要只到 \(n\) ,要到 \(n*m\) 。不然会寄。
  3. 在遍历的时候要从 \(b\) 开始遍历,不然可能会有问题(不太记得写对没有了,应该会有问题吧)

核心代码

while (Q --){
	lwl t = 0;
	for (int i = 1; i <= a; i ++){
		scanf("%s",str + 1);
		for (int j = 1; j <= b; j ++)
			t = t * P + str [j] - '0';
	}
	s.insert(t);
	q.push_back(t);
}

for (int i = b; i <= m; i ++){
	lwl t = 0;
	int l = i - b + 1,r = i;
	for (int j = 1; j <= n; j ++){
		t = t * p[b] + get(has[j], l, r);
		if (j - a > 0)
			t -= get(has[j - a],l,r) * p[a * b];
		if (j >= a)
			s.erase(t);
	}
}

for (auto t : q){
	if (!s.count(t))
		puts("1");
	else wj;
}

电话列表

也是 \(ACwing\)上面的题,题号 \(161\)

这题就是一个很简单的 \(trie\) 树,肥肠的基础啊。注意一下多测清空就可以了

然后还是贴一小段

bool insert(){
	int p = 0;
	bool xin = false,zd = false;
	int siz = strlen(str);
	for (int i = 0; i < siz; i ++){
		int &s = ne[p][(str[i] - '0')];
		if (!s){
			s = ++idx;
			xin = true; 
		}
		p = s;
		if (flag[p]) zd = true;
	}
	flag[p] = true;
	return (!zd) && xin;
}

1801 黑匣子

这题在 \(ACwing\) 上面叫黑盒子,是不是洛谷和 \(ACwing\)有仇(不是)

这题应该算是一个模拟吧,就是先把所有数都存下来,虽然我感觉好像在线操作也是可以搞的,但是懒得写了。

然后就是用两个堆把数给存起来, 保证 $ up$ 的size就好了,这样子 \(down\) 的堆顶就是这些数中间的第 \(k\) 个。而且显而易见,这个 \(get\) 数组是单调不减的,所以也不用搞那些奇奇怪怪的东西,非常的淳朴啊

核心代码

for (int i = 1; i <= m; i ++){
	while (k <= gett[i]){
		if (down.empty() || w[k] >= down.top())
			up.push(w[k]);
		else{
			up.push(down.top());
			down.pop();
			down.push(w[k]);
		}
		k ++;
	}
	down.push(up.top());
	up.pop();
	fw(down.top());
	ch;
}

生日礼物

这一题洛谷上好像没有,是 $ ACwing $ 上面的,题号 $ 163 $

题目描述:

翰翰 $ 18 $ 岁生日的时候,达达给她看了一个神奇的序列 $ A_1,A_2,…,A_N $ 。

她被允许从中选择不超过 $ M $ 个连续的部分作为自己的生日礼物。

翰翰想要知道选择元素之和的最大值。

这一题可以转化为数据备份那样的题目。

首先,显而易见的是,如果连续的正数段要选的话肯定要全部选,如果连续的负数段要选的话肯定是为了连接起两端的正数段,所以一定会全选,于是就可以将一段连续的正数段或者负数段合并为一段。

接下来我们将所有正数段都加到 $ ans $ 里面去,然后记录一下选了几段为 $ cnt $ 。现在就有两种情况,第一种是如果 $ cnt $ < $ m $ ,那么直接输出就行了

第二种是如果 $ cnt $ > $ m $ ,那么需要做的就是删除一段正数或者增加一段负数

  1. 增加一段负数的话,就相当于减去这段负数的绝对值

  2. 减去一段正数也相当于减去这段正数的绝对值

所以这时候就变成了和数据备份一样的题了。(数据备份在飞书23.2.16)

核心代码

for (int i = 1; i <= n; i ++){
	if (a[i] == 0) continue;
	if (!cnt || (lwl)a[i] * w[cnt] < 0){
		cnt ++;
		w[cnt] = a[i];
	} else w[cnt] += a[i];
}
	
n = cnt,cnt = 0;
priority_queue<pii,vector<pii>,greater<pii> >q;
for (int i = 1; i <= n; i ++){
	if (w[i] > 0){
		ans += w[i];
		cnt ++;
	}
	l[i] = i - 1;
	r[i] = i + 1;
	q.push({abs(w[i]),i});
}

匹配统计

$ ACwing $ 上面的题,这题理论上来说用 $ KMP $ 快一些,但是确实是不太喜欢 $ KMP $ ,于是用哈希和二分水了一个过去。

在每个问题中,他给定你一个整数 $ x $ ,请你告诉他有多少个位置,满足“字符串 $ A $ 从该位置开始的后缀子串”与 $ B $ 匹配的长度恰好为 $ x $ 。

例如: $ A=aabcde $ , $ B=ab $ ,则 $ A $ 有 $ aabcde $ 、 $ abcde $ 、 $ bcde $ 、 $ cde $ 、 $ de $ 、 $ e $ 这 $ 6 $ 个后缀子串,它们与 $ B=ab $ 的匹配长度分别是 $ 1 $ 、 $ 2 $ 、 $ 0 $ 、 $ 0 $ 、 $ 0 $ 、 $ 0 $ 。

然后用字符串哈希做就非常的简单,先把两个字符串的哈希值求出来,然后遍历 $ a $ 的每一个后缀(虽然是从后往前遍历的),然后用二分找出 $ b $ 的最长匹配长度就可了,再用一个 $ cnt $ 数组记录一下,最后问的时候直接输出就可以乐

核心代码

bool check(int be,int k){
   return get(a,be,k) == get(b,1,k - be + 1);
}

for (int i = 1; i <= n; i ++){
   int l = i, r = i + m;
   if (r > n + 1) r = n + 1;
   while (l < r){
   	int mid = (l + r) >> 1;
   	if (check(i,mid)) l = mid + 1;
   	else r = mid; 
   }
   cnt[l - i] ++;
}

P1016 旅行家的预算

这一题其实算是贪心,但是因为贪心是不需要证明的,所以就没有单独开一个地方放,然后这一题刚好用了单调队列,所以就甩到数据结构这里来乐

这个题存数据的时候,我是先用的一个 $ pair<int,int> $ 存了一下车站,然后排个序再扔到数组里面去。而且把终点和起点都存进去了,显而易见的是终点是没有加油的价格的,而起点是没有 $ dis $ 的,所以正好就用 $ 0 $ 就可以了。

然后首先就是判断能不能到达,这个比较简单,因为排了序,只要看满油的时候能不能走完这一段路程就可以了。

然后捏我们开一个双端队列(单调队列)来存一下油箱里面的油,具体的参数就是油的体积和油的价格,为什么要用单调队列捏,因为后面会把有些油给退回去,所以要把每一个加油站的油分开存。

然后因为我们可以把油退回去,所以我们每到一站不管三七二十一就把油箱给装满就可以了。要从上一个车站到这一个车站是需要油的,所以就从价格便宜的油开始消耗就可以了,这里需要注意的是如果没有消耗完还要把没有消耗完的油加到单调队列里面去(因为耗油的时候是把这些油全部都弹出来了的)

然后单调队列的时候就判断当前队列最后的油的单价,如果当前队列最后的油的单价大于当前车站的油价的话,就用当前车站的油代替那些油。最后要注意的就是到了终点站要把多余的油都退回去。

核心代码

   ans += u * p[0];
   q.push_back({u,p[0]});
   for (int i = 1; i <= n; i ++) {
   	double need = (dist[i] - dist[i - 1]) / v;
   	while (q.size() && need > 0) {
   		auto t = q.front();
   		q.pop_front();

   		if (t.fi > need) {
   			u -= need;
   			q.push_front({t.fi - need,t.se});
   			break;
   		}

   		u -= t.fi,need -= t.fi;
   	}

   	if (i == n) {
   		while (q.size()) {
   			ans -= q.front().fi * q.front().se;
   			q.pop_front();
   		}
   	}

   	while (q.size() && q.back().se > p[i]) {
   		ans -= q.back().fi * q.back().se;
   		u -= q.back().fi;
   		q.pop_back();
   	}
   	ans += (c - u) * p[i];
   	q.push_back({c - u,p[i]});
   	u = c;
   }
posted @ 2023-06-01 15:50  jingyu0929  阅读(16)  评论(0)    收藏  举报