首页 > 其他分享 >双指针和双向搜索

双指针和双向搜索

时间:2023-07-13 09:13:55浏览次数:32  
标签:return 端点 int money mid 搜索 lwl 双向 指针

双指针

 也常叫 \(two-pointers\) ,是一种简单又灵活的技巧和思想,单独使用可以轻松解决一些特定问题,和其他算法结合也能发挥多样的用处。

 顾名思义,就是同时使用两个指针,在序列、链表结构上指向的是特定位置,在树、图上指向的是节点,通过同向移动,或者相向移动来维护、统计信息。

例题

 给定一个存在环的单向链表,需要找出环的大小和环的起点

\(solution\) :首先两个指针只想链表的头部,令一个指针一次走一步,另一个指针一次走两步,如果他们相遇,证明有环,否则无环

 显而易见,相遇的时候慢指针走过的步数就是环的长度的整数倍。

 利用这个等式,可以在两个指针相遇后,将其中一个指针一道表头,让两者都一步一步走,再度相遇的位置即为环的起点。

 大小的话相遇之后再跑一圈就可以了

双向搜索

 双向同时搜索:基本思路是从状态图上的起点和终点同事开始进行广搜或者深搜,如果发现搜索的两端相遇了,那么可以认为是获得了可行解

 Meet in the middle:主要思想是将整个搜索过程分成两半,分别搜索,最后将两半的结果合并。经过这个算法可以将复杂度的指数减半,也就是减少了搜索的深度。

例题 P1429 平面最近点对

 给定平面上n个点,找出其中一对点对的距离,使得在这n个点的所有点对中,该距离为所有点对中最小的,其中\(n \le 10^5\)

\(solution\)

 先将所有点按照x坐标排序。考虑分治。先递归计算两个大小相同的子区间,分别得到两个答案,设 \(d = \min(d1,d2)\) ,考察跨过中线的点对

 对于1区间中的某个点,2区域中可供他选择的范围是一个\(d \times 2d\) 的矩形,由于2区间中两点的间距必然不小于 \(d\) ,故至多有六个待选点

 故对区域内点按照 y 坐标归并排序,在归并排序的过程中,就可以线性扫描计算这部分贡献。

int n;
pii w[N];

double dis(int i,int j) {
	double dx = w[i].fi - w[j].fi;
	double dy = w[i].se - w[j].se;
	return sqrt(dx * dx + dy * dy);
}

double get(int l,int r) {
	if (l >= r) return dinf;
	int mid = (l + r) >> 1;
	double d = min(get(l,mid),get(mid + 1,r));
	
	for (int i = mid; i >= l; i --) {
		if (w[mid + 1].fi - w[i].fi >= d) break;
		for (int j = mid + 1; j <= r; j ++) {
			if (w[j].fi - w[i].fi >= d) break;
			d = min(d,dis(i,j));
		}
	}
	
	return d;
}

int main(){
	n = fr();
	for (int i = 1; i <= n; i ++) {
		w[i].fi = fr();
		w[i].se = fr();
	}
	sort(w + 1,w + 1 + n);
	double ans = get(1,n);
	printf("%.4lf",ans);
	return 0;
}

例题

2962 Lights G

 因为 \(n \le 40\),所以可以想到用双向搜索, 搜索的时候就从初始状态(全 \(0\) )把所有前面一半可能的情况都跑一遍,然后再从终止状态(全 \(1\) )的时候dfs一遍,把后面的一半情况跑了。

 然后在前面跑的时候记录一下到这个点要经过多少次变化,后面一次跑的时候如果碰到了和前面一样的情况,那么就代表着这两个状态合在一起可以点亮全部灯。然后再取一个 \(min\) 就可以了。

 这里需要注意的一点就是,判断当前状态的时候要用 \(lwl\) 不然会超 \(int\) 变成负数。还有就是不能单纯的判断当前这个点所对应的 \(map\) 值是不是 \(0\) ,因为有可能后面一半操作就可以点亮所有的灯,所以特判一下。然后我这里是直接一开始赋值为 \(1\),后面再减。

int n,m;
vector<int> e[N];
map<lwl,int> dis;
int ans = inf;

void dfs1(int u,lwl t,int cnt) {
	if (dis[t]) dis[t] = min(dis[t],cnt);
	else dis[t] = cnt;
	if (u > n / 2) return ;
	dfs1(u + 1,t,cnt);
	t ^= ((lwl)1 << (u - 1));
	for (auto i : e[u]) {
		t ^= ((lwl)1 << (i - 1));
	}
	dfs1(u + 1,t,cnt + 1);
}

void dfs2(int u,lwl t,int cnt) {
	if (dis[t]) ans = min(ans,cnt + dis[t]);
	if (u > n) return ;
	if (ans <= cnt) return ;
	
	dfs2(u + 1,t,cnt);
	t ^= ((lwl)1 << (u - 1));
	for (auto i : e[u]) {
		t ^= ((lwl)1 << (i - 1));
	}
	dfs2(u + 1,t,cnt + 1);
}

int main(){
	n = fr(),m = fr();
	int a,b;
	for (int i = 1; i <= m; i ++) {
		a = fr(),b = fr();
		e[a].push_back(b);
		e[b].push_back(a);
	}
	dfs1(1,0,1);
	dfs2(n / 2 + 1,((lwl)1 << n) - 1,0);
	fw(ans - 1);
	return 0;
}

3066 Running Away From the Barn G

 这个题就是找一下自己到上面哪一个点的距离刚好比k大,然后这一段区间他都有一个1的贡献,用二分和树上差分就可以了。老师讲的是双指针,但是没有听懂。

int n;
lwl k;
int fa[N][20],de[N];
lwl w[N],dis[N];
int cf[N];

void get(int u) {
	cf[u] ++;
	lwl qwq = k;
	for (int i = log2(de[u]); i >= 0; i --) {
		if (dis[u] - dis[fa[u][i]] <= qwq) {
			qwq -= dis[u] - dis[fa[u][i]];
			u = fa[u][i];
		}
	}
	cf[fa[u][0]] --;
}

int main(){
	n = fr(),k = fr();
	for (int i = 2; i <= n; i ++) {
		fa[i][0] = fr();
		w[i] = fr();
		dis[i] = w[i] + dis[fa[i][0]];
	}
	for (int i = 1; i <= n; i ++) {
		de[i] = de[fa[i][0]] + 1;
		for (int k = 1; k <= log2(de[i]); k ++)
			fa[i][k] = fa[fa[i][k - 1]][k - 1];
	}
	for (int i = 1; i <= n; i ++) {
		get(i);
	}
	for (int i = n; i; i --) {
		cf[fa[i][0]] += cf[i];
	}
	for (int i = 1; i <= n; i ++) {
		fw(cf[i]);
		ch;
	}
	return 0;
}

练习

 今天的题有点难想,但是数据极弱,第二题 \(n^2\) 暴力就可以水过去,第三题直接 \(puts('0')\) 就有五十分。第一题不知道数据咋样,写的正解。

A.囊中羞涩

 看题目范围 \(n \le 40\) 可以想到用双向搜索。

 这个题就直接把前面一半的情况都处理一下,然后记录一下用一下前缀和,后面算的时候找到 \(m - money\) 对应的前面的情况再累加一下就可以了。

int n;
lwl m,idx;
lwl w[N];
set<lwl> s;
int sum[1 << 21];
lwl dy[1 << 21];
lwl ans;
map<lwl,int> cnt;

void dfs1(int u,lwl money,int flag) {
	if (money > m) return ;
	cnt[money] += flag;
	s.insert(money);
	if (u > n / 2) return ;
	dfs1(u + 1,money,0);
	money += w[u];
	dfs1(u + 1,money,1);
}

void dfs2(int u,lwl money,int flag) {
	if (flag) {
		lwl l = 1, r = idx;
		while (l <= r) {
			lwl mid = (l + r) >> 1;
			if (dy[mid] > m - money) r = mid - 1;
			else l = mid + 1;
		}
		ans += sum[r];
	}
	if (u > n) return ;
	dfs2(u + 1,money,0);
	money += w[u];
	dfs2(u + 1,money,1);
}

int main(){
	n = fr();
	m = fr();
	for (int i = 1; i <= n; i ++) {
		w[i] = fr();
	}
	cnt[0] = 1;
	dfs1(1,0,0);
	idx = 0;
	for (auto i : s) {
		dy[++ idx] = i;
	}
	for (int i = 1; i <= idx; i ++) {
		sum[i] = sum[i - 1] + cnt[dy[i]];
	}
	dfs2(n / 2 + 1,0,1);
	fw(ans);
	return 0;
}

B.交换人生

 这个序列如果前面一个端点定下来的话,是一个单调不减的序列,所以可以用二分找出。

 因为如果这个数能给答案提供贡献,那么前面的异或值要有至少一位之前的异或值对应的是 \(0\) ,而这个数这一位对应的是 \(1\) 。

 所以我们就枚举起点,然后这个起点所对应的最大值就是这个点到 \(n\) 这一段的答案,这个值是一样的。如果当前求出来的值比之前的 \(ans\) 要大,我们就二分找出这个值在这个序列里面对应的最小值,然后更新答案。

 昨天搞第三题去了这题的代码没有写,就贴贴 \(xyzfrozen\) 的代码()

#define int long long
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x])

int get(int p)
{
	int l=p,r=n,res=n;
	while(l<=r) 
	{
		int mid=(l+r)>>1;
		if(s[mid]-s[p-1]-(d[mid]^d[p-1])>=ans) r=mid-1,res=mid;
		else l=mid+1;
	}
	return res;
}

void solve()
{	
	ans=s[n]-d[n],L=1,R=n;
	for(int i=1;i<=n;i++)
	{
		int val=s[n]-s[i-1]-(d[n]^d[i-1]);
		if(val>ans) ans=val,L=i,R=get(i);
		else if(val==ans)
		{
			int tR=get(i);
			if(tR-i<R-L || (tR-i==R-L && i<L)) L=i,R=tR;
		}
	}
	fw(L),pt,fw(R);
}

signed main()
{
	n=fr();
	for(int i=1;i<=n;i++) a[i]=fr();
	for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i],d[i]=d[i-1]^a[i];
	solve();
	return 0;
}

C.交换人生

原题 Loj353 花火

 这一题正解真他妈难想。老师讲了两遍才懂。

 首先观察这个序列,如果他没有交换任意两个元素的话,只交换相邻两个元素就是求逆序对的数量,如果交换两个元素的话,他提供的贡献就是减少了序列中逆序对的数量。

 首先要考虑到这个交换在一开始交换或者其他时候交换其实提供的贡献是一样的。(如果中间交换其实可以先一开始交换再按照原来的方法交换,效果是一样的)。

 考虑交换两个元素可以提供的贡献。设交换 \(w[i],w[j]\) \(i < j\) ,那么可以发现的是如果 \(w[i] < w[j]\) 的话是没有交换的必要的,所以我们要交换 \(w[i] > w[j]\) 的元素。如果交换这两个元素,其实对于这两个元素旁边的两个元素是没有影响的,那么考虑中间的元素。如果中间的元素同小于或者同大于这两个数,那么逆序对的数量是不会改变的,那么对于值在他们中间的数并且位置在他们中间的数,就可以减少两个逆序对。

 然后我们就可以把这一题抽象到坐标系中,横坐标代表位置,纵坐标代表值。就如下图

 那么问题就抽象为了找到两个点并且以这两个点为左上和右下端点构造矩形,使其框住的点最多。

 那么这个时候,我们先找到潜在的左上端点和右下端点,因为图中显然不管怎么选,6点作为右下端点一定优于5点作为右下端点,2点作为左上端点一定优于3点作为左上端点,所以这个时候我们用单调栈可以找出所有最优的左上端点和右下端点,处理出来的线差不多就是这个样子(下图蓝点):

 然后我们考虑每一个点能给他们的贡献。观察图中的绿点,绿点可以给绿色括号中的上面三个蓝色点一个贡献,给下面三个蓝色点一个贡献,这个时候可以二分找出蓝色点的边界在哪里。

 然后我们进一步抽象,可以再建立一个坐标系,横坐标是左上端点,纵坐标是右下端点,每一个这样的绿点其实在图中可以对应成一个矩形,我们要求的就是图中哪一个点被矩形覆盖的次数最多。这个时候就转化成了扫描线的问题。

 扫描线的话就用线段树就可以了。就是给每一条边一个边权,左边的边给 \(1\) ,右边的边给 \(-1\),然后这样扫过去,线段树就维护一下区间最大值就可以了,具体看 \(ACwing248\) 窗内的星星。然后求一开始的逆序对的话用归并排序就可以了。最后求出来点最多的覆盖次数减的时候记得要乘二

// 线段树
struct node{
	int maxn,laz;
}tr[N << 2];

// 扫描线
struct Node{
	int l,r,h,w;
}line[N << 1];

int n,cnt;
int w[N],temp[N];
int ls[N],rx[N]; // 左上右下的点集

lwl get(int l,int r) {
	if (l >= r) return 0;
	int mid = (l + r) >> 1;
	lwl ans = get(l,mid) + get(mid + 1,r);
	int k = 0,i = l,j = mid + 1;
	while (i <= mid && j <= r) {
		if (w[i] < w[j]) temp[k ++] = w[i ++];
		else {
			temp[k ++] = w[j ++];
			ans += (mid - i + 1);
		}
	}
	while (i > mid && j <= r) temp[k ++] = w[j ++];
	while (i <= mid && j > r) temp[k ++] = w[i ++];
	for (int i = l, j = 0; i <= r; i ++, j ++) {
		w[i] = temp[j];
	}
	return ans;
}

bool cmp(Node a,Node b) {
	if (a.h == b.h) return a.l < b.l;
	return a.h < b.h;
}

void pushup(int idx) {
	tr[idx].maxn = max(tr[il].maxn,tr[ir].maxn) + tr[idx].laz;
}

void modify(int L,int R,int l,int r,int idx,int x) {
	if (L > R) return ;
	if (L >= l && R <= r) {
		tr[idx].maxn += x;
		tr[idx].laz += x;
		return ;
	}
	int mid = (L + R) >> 1;
	if (mid >= l) modify(L,mid,l,r,il,x);
	if (mid < r) modify(mid + 1,R,l,r,ir,x);
	tr[idx].maxn = max(tr[il].maxn,tr[ir].maxn) + tr[idx].laz;
}

int main(){
	freopen("qwq.in","r",stdin);
	n = fr();
	for (int i = 1; i <= n; i ++) {
		w[i] = fr();
	}
	ls[0] = -inf,rx[n + 1] = inf;
	for (int i = 1; i <= n; i ++)
		ls[i] = max(ls[i - 1],w[i]);
	for (int i = n; i; i --)
		rx[i] = min(rx[i + 1],w[i]);
	for (int i = 1; i <= n; i ++) {
		int l = lower_bound(ls + 1,ls + 1 + n,w[i]) - ls;
		int r = lower_bound(rx + 1,rx + 1 + n,w[i]) - rx;
		if (l >= i || r <= i) continue;
		if (rx[r] > w[i]) r--;
		line[++cnt] = {l,i - 1,i + 1,1};
		line[++cnt] = {l,i - 1,r + 1,-1};
	}
	sort(line + 1,line + 1 + cnt,cmp);
	lwl t = 0;
	for (int i = 1; i <= cnt; i ++) {
		modify(1,n,line[i].l,line[i].r,1,line[i].w);
		t = max(t,tr[1].maxn);
	}
	lwl ans = 0;
	ans = get(1,n);
	fw(ans - t * 2);
	return 0;
}

标签:return,端点,int,money,mid,搜索,lwl,双向,指针
From: https://www.cnblogs.com/jingyu0929/p/17549434.html

相关文章

  • 向量数据库的崛起:从矢量搜索到深度学习 (二)
    前言在上一节中,我们简要介绍了向量数据库的背景以及对非结构化数据进行向量化的方法,即Embedding。那么我们如何将这些特征向量应用于搜索任务呢?在搜索任务中,最常见的情况是从数据库中查找与给定向量最相似的数据。因此,我们需要一种能够衡量向量之间相似程度的算法,这也是本节将要......
  • 算法小菜鸟成长记录Day01-二分查找和双重指针
    二分查找和双重指针今天是代码随想录刷题的第一天,刚开始刷的时候昏昏欲睡,其中用时3h主要实现以下几个部分二分查找:其中二分查找中其收获最大部分就在于对左开右闭区间的理解,如果都是闭区间也就是【a,b】,那么在while中的条件就为while(left<=right),确保其中是拥有元素也就是......
  • 野指针与空指针
    一、野指针与空指针要注意(1)野指针野指针是一个指向未知(undefine)的,不确定地方的指针."未知的,不确定的",指向的地方可能存在,可能不存在.可能可以访问,也可能不可以访问.对野指针的访问,会有后果?可能可以访问,可能不可以访问(导致非法的内存访问).非法的内存......
  • 一个突破搜索引擎的信息茧房方法
    搜索引擎为我们提供了一个广阔的知识世界,通过基于关键词的搜索算法,帮助我们快速、准确地找到所需的信息,包括文章、新闻、图片、视频等各种类型的内容。所以我们要善于使用搜索引擎,帮助我们探索知识、寻找答案和解决问题,扩展自己的知识储备和理解世界。突破搜索引擎的信息茧房在......
  • 数据结构与算法 #18 下跳棋,极富想象力的同向双指针模拟
    ⭐️本文已收录到AndroidFamily,技术和职场问题,请关注公众号[彭旭锐]和[BaguTreePro]知识星球提问。学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭将基于Java/Kotlin语言,为你分享常......
  • elementUI远程搜索功能遇到的坑(el-autocomplete) 如果是提前加载出全量数据 去掉v-mode
    elementUI远程搜索功能遇到的坑(el-autocomplete)如果是提前加载出全量数据去掉v-model.trim换为v-model=“nameinputvalue”原文链接:https://blog.csdn.net/CuiCui_web/article/details/95939746本文主要是解决下拉框根据返回值隐藏   动态设置建议列表值等问题结构写......
  • 指针函数与函数指针
    1.指针函数先看下面的函数声明,注意,此函数有返回值,返回值为int*,即返回值是指针类型的。1.int*f(inta,intb);上面的函数声明又可以写成如下形式:int*f(inta,intb);让指针标志*与int紧贴在一起,而与函数名f间隔开,这样看起来就明了些了,f是函数名,返回值类型是一个i......
  • Find a way bfs搜索 容易出错
    题目链接:题意:给你一个图,图中有不能走的障碍物,和两人,以及n个(n>=1)KFC,现在要求找到其中一个KFC,让两个人人走到这个KFC的时间总和最小;#include<iostream>#include<cstdio>#include<cstring>#include<cstdlib>#include<cmath>#include<vector>#include<queue>......
  • 106.你什么情况用指针当参数,什么时候用引用,为什么?
    106.你什么情况用指针当参数,什么时候用引用,为什么?1.使用引用参数的主要原因有两个1.程序员能修改调用函数中的数据对象2.通过传递引用而不是整个数据–对象,可以提高程序的运行速度2.一般的原则1.对于使用引用的值而不做修改的函数:(1)如果数据对象很小,如内置数据类型或者小型结......
  • 74.指针加减计算要注意什么?
    74.指针加减计算要注意什么?指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的,因此在涉及到指针加减运算需要十分小心,加多或者减多都会导致指针指向一块未知的内存地址,如果再进行操作就会很危险。举个例子:#include<iostream>usingnamespacestd;intmain(......