2022“杭电杯”中国大学生算法设计超级联赛(4)部分题题解

题意很简单就是给定一个有向图,每条边两个权值,要求从1到n,在第一个权值最小的情况下第二个权值最大。
第一反应不就是spfa吗?这肯定会T.....(spfa已经死了)
然后就寄了....
赛后看题解,最短路图?之前没了解过啊...
最短路图就是将两个点之间的最短路的所有路径保存下来。这样,我从1号点在最短路图上跑,无论怎么跑,只要走到了n号点一定是最短路。
在这个最短路图的基础上,考虑最长路。(不要说spfa...)既然spfa和dij都不行的情况下,只要能用DP的法子。重新看看题面,发现我们保留下来的可能会有\(e_i,p_i\)都为0的环。既然权值是0,那么对于我们最长路而言便没有影响。我们tarjan强连通分量缩点,之后就是无环DAG,放心DP即可。(这看起来就难写...看起来其实就是将各种知识点杂到一起,但真的写起来的时候,数组就开得真TM多...)

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10,M=3e5+10;
const ll INF=1e18;
int n,m,T,tot,link[N],vis[N],du[N];
ll d[N][2],ans_min,ans_max;
int dfn[N],low[N],Stack[N],num,ins[N],cnt,top,c[N];
struct node{int y,next,e,p;}a[M<<1];
vector<pair<int,int> >son[N],to[N];
vector<int>scc[N];
inline void add(int x,int y,int e,int p)//正图是奇数,反图是偶数. 
{
	a[++tot].y=y;a[tot].e=e;a[tot].p=p;a[tot].next=link[x];link[x]=tot;
	a[++tot].y=x;a[tot].e=e;a[tot].p=p;a[tot].next=link[y];link[y]=tot;
}
inline void dijkstra(int s,int id)
{
	memset(vis,0,sizeof(vis));
	priority_queue<pair<ll,int> >q;
	for(int i=1;i<=n;++i) d[i][id]=INF;
	d[s][id]=0;q.push({0,s});
	while(q.size())
	{
		int x=q.top().second;q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(int i=link[x];i;i=a[i].next)
		{
			if(i%2!=id) continue;
			int y=a[i].y;
			if(d[y][id]>d[x][id]+a[i].e)
			{
				d[y][id]=d[x][id]+a[i].e;
				q.push({-d[y][id],y});
			}
		}
	}
}
inline void tarjan(int x)
{
	dfn[x]=low[x]=++num;
	ins[Stack[++top]=x]=1;
	for(auto t:son[x])
	{
		int y=t.first;
		if(!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);	
		}	
		else if(ins[y]) low[x]=min(low[x],dfn[y]);
	}	
	if(dfn[x]==low[x])
	{
		cnt++;int y;
		do
		{
			y=Stack[top--],ins[y]=0;
			c[y]=cnt,scc[cnt].push_back(y);
		}while(x!=y);
	}
}
inline void topsort()
{
	queue<int>q;
	q.push(c[1]);du[c[1]]=0;
	while(q.size())
	{
		int x=q.front();q.pop();
		for(auto t:to[x])
		{
			int y=t.first;
			d[y][0]=max(d[y][0],d[x][0]+t.second);
			if(--du[y]==0) q.push(y); 
		}
	}
	
}
int main()
{
//  	freopen("1.in","r",stdin);
    scanf("%d",&T);
    while(T--)
    {
    	scanf("%d%d",&n,&m);
    	memset(link,0,sizeof link);
    	tot=0;
    	for(int i=1;i<=m;++i)
    	{
    		int x,y,e,p;
    		scanf("%d%d%d%d",&x,&y,&e,&p);
    		add(x,y,e,p);
		}
		dijkstra(1,1);ans_min=d[n][1];
		dijkstra(n,0);
		for(int i=1;i<=n;++i) son[i].clear();
		for(int x=1;x<=n;++x)
		{
			for(int i=link[x];i;i=a[i].next)
			{
				if(i%2==0) continue;
				if(d[x][1]+a[i].e+d[a[i].y][0]==ans_min)
					son[x].push_back({a[i].y,a[i].p});
			}
		}
		memset(dfn,0,sizeof dfn);
		memset(low,0,sizeof low);
		memset(ins,0,sizeof ins);
		for(int i=1;i<=cnt;++i) scc[i].clear();
		cnt=num=top=0;
		tarjan(1);
		memset(du,0,sizeof du);
		for(int i=1;i<=cnt;++i) to[i].clear();
		for(int x=1;x<=n;++x)
		{
			for(auto t:son[x])
			{
				int y=t.first;
				if(c[y]!=c[x])
				{
					to[c[x]].push_back({c[y],t.second});
					du[c[y]]++;
				}	
					
			} 
		}
		memset(d,0,sizeof d);
		topsort();
		printf("%lld %lld\n",ans_min,d[c[n]][0]);
	}
    return 0;
}

Magic

读完题意之后我们可以写出以下题目需要我们满足的条件:
我们设\(a[i]\)表示塔楼\(i\)所用到的原料。
则满足:\(a[l_i]+a[l_i+1]+...+a[r_i]<=B_i\)
\(a[i-k+1]+a[i-k+2]+...+a[i+k-1]>=p_i\)
我们发现这两个式子都是区间和的形式,我们用前缀和数组进行调整一下为:
\(sum[r_i]-sum[l_i-1]<=B_i\)
\(sum[i+k-1]-sum[i-k]>=p_i\)同时由于前缀和的性质,我们还需要满足
\(sum[i]>=sum[i-1]\)然后我们的目标是在满足上述条件的情况下,\(sum[n]\)最小。
看到上述式子,能不能想到一些算法?
对,差分约束(我都快2年没见过这个算法的题了)。
差分约束系统指的是给定N个变量,以及M个约束条件,每个约束条件由两个变量做差构成。这不和上述式子一模一样吗?
我们可以将上述式子都改成同样的形式。
\(sum[r_i]<=sum[l_i-1]+B_i\)
\(sum[i-k]<=sum[i+k-1]-p_i\)
\(sum[i-1]<=sum[i]+0\)
然后求\(sum[n]\)最小。
我们跑最短路,建边即可。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=10010;
int T,n,k,p[N],link[N],tot,cnt[N];
ll d[N];
bool vis[N];
struct wy{int y,next;ll v;}a[N<<2];
inline void add(int x,int y,ll v)
{
	a[++tot].y=y;a[tot].v=v;a[tot].next=link[x];link[x]=tot;
}
inline bool spfa()
{
	queue<int>q;
	for(int i=1;i<=n;++i) q.push(i),vis[i]=1;
	while(q.size())
	{
		int x=q.front();q.pop();vis[x]=0;
		for(int i=link[x];i;i=a[i].next)
		{
			int y=a[i].y;
			if(d[y]>d[x]+a[i].v)
			{
				d[y]=d[x]+a[i].v;
				cnt[y]=cnt[x]+1;
				if(!vis[y]) q.push(y),vis[y]=1;
				if(cnt[y]>=n) return false;
			}
		}	
	} 
	return true;
}
int main()
{
//	freopen("1.in","r",stdin);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&k);
		for(int i=0;i<=n+1;++i)
		{
			link[i]=0;
			cnt[i]=0;
			d[i]=0;
			vis[i]=0;
		}
		tot=0;
		for(int i=1;i<=n;++i) 
		{
			scanf("%d",&p[i]);
			int x=min(i+k-1,n),y=max(i-k,0);
			add(x,y,-p[i]);	
		}
		int q;scanf("%d",&q);
		for(int i=1;i<=q;++i)
		{
			int l,r,B;
			scanf("%d%d%d",&l,&r,&B);
			add(l-1,r,B);
		}
		for(int i=1;i<=n;++i) add(i,i-1,0);
		if(!spfa()) puts("-1");
		else printf("%lld\n",d[n]-d[0]); 
	}
	return 0;
}

记得之前在牛客多校上做过这个题的弱化版,那个是直接枚举DP跑最小值,这次是最大的连续世界区间跑方案数。
首先我们可以从暴力出发,找找一些性质。(一切皆可暴力)
加入我们固定起点为s后,设f[i][j]表示从s到i第j号点上的方案数.
则初始化为f[s][1]=1.
在i-1这个世界里,存在k到j的边。
f[i][j]=f[i-1][j]+f[i-1][k];
通过这个DP式子,我们发现随着区间的扩展,方案数是不断变大的。并且题目要求的也是在n号点的方案数小于等与k的最大连续区间。
对于最大连续区间的问题,有一个比较经典的思想就是双指针法,我们观察这道题符不符合指针单调的性质。当我们固定一个l,扩展到最大的r之后,我们l++,这个时候区间减少了,那么方案数也只能减少,则当前区间也一定满足题意,我们r只需增大即可,符合指针单调,可以采用双指针。
双指针的总的方针确定后,考虑如何维护这个区间,考虑暴力DP的话,发现无法取消影响(当我们l向前的时候我们需要首先取消l的影响才行)。并且观察题目范围,m只有20,这么小的数据,难道是...矩阵?再回去观察我们的式子,发现这个东西确实可以用矩阵乘法去维护。那撤销操作我们直接乘上逆矩阵不就行了?等等,那万一某个世界的矩阵不存在逆矩阵怎么办?
考虑怎么避开这个删除的操作,这里用到的技巧是再加入一个指针lim,他是存在于l,r之间的,我们处理出b[i]表示i到lim的矩阵之间相乘的结果,再用一个变量base表示lim+1到r之间矩阵相乘的结果,那么l到r之间矩阵相乘答案就是b[l]*base.当我们的l越过lim的时候,这个时候lim就直接往后跳,一直跳到r,并且将l到r之间的b给搞出来。可以发现lim也是单调的,所以加入lim后双指针的复杂度还是不变的。
总的复杂度为\(O(nm^3)\).
这个方法感觉和回滚莫队的处理有点类似,但并不完全相同。但都是为了避免减法操作的小技巧。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e3+10,M=23;
int n,m,K,T;
struct wy
{
	ll a[M][M];
	wy() {memset(a,0,sizeof a);}
	inline void clear() 
	{
		for(int i=1;i<=m;++i) 	
			for(int j=1;j<=m;++j) a[i][j]=0;
	}
	inline void dan()
	{
		for(int i=1;i<=m;++i)
			for(int j=1;j<=m;++j) 
			{
				if(i==j) a[i][j]=1;
				else a[i][j]=0;
			}
	}	
	wy friend operator *(wy a,wy b)
	{
		wy c;
		for(int i=1;i<=m;++i)
			for(int j=1;j<=m;++j)
				for(int k=1;k<=m;++k)
				{
					c.a[i][j]+=a.a[i][k]*b.a[k][j];
					if(c.a[i][j]>K) c.a[i][j]=K+1;
				}
		return c;		
	}
}A[N],B[N];
inline bool check(wy a,wy b)
{
	ll ans=0;
	for(int i=1;i<=m;++i)
	{
		ans+=a.a[1][i]*b.a[i][m];
		if(ans>K) return false;
	}
	return true;
}
int main()
{
//	freopen("1.in","r",stdin);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d%d",&n,&m,&K);
		for(int i=1;i<=n;++i)
		{
			A[i].dan();
			int l;scanf("%d",&l);
			for(int j=1;j<=l;++j) 
			{
				int u,v;scanf("%d%d",&u,&v);
				A[i].a[u][v]=1;
			}
		}
		int ans=0;
		wy base;//base存lim+1-r之间矩阵的乘积,b[l]-b[lim]是已知. 
		for(int l=1,lim=0,r=1;l<=n;++l)
		{
			if(n-l+1<=ans) break;
			if(l>lim)
			{
				B[r]=A[r];
				for(int i=r-1;i>lim;--i) B[i]=A[i]*B[i+1];
				lim=r;base.dan();
			}
			wy cd=B[l]*base;//b[l]表示l-lim的累乘. 
			while(r+1<=n&&check(cd,A[r+1]))
			{
				++r;
				base=base*A[r];
				cd=B[l]*base;
			}
			ans=max(ans,r-l+1); 
		}
		printf("%d\n",ans);
	}
	return 0;
}

刚开始被卡常数卡了许久....自带大常数的debuff...
最后还是带着多年搜索剪枝的功底,把它卡过去了。真气人。。

posted @ 2022-07-29 09:36  逆天峰  阅读(114)  评论(0编辑  收藏  举报
作者:逆天峰
出处:https://www.cnblogs.com/gcfer//