• 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...
    最后还是带着多年搜索剪枝的功底,把它卡过去了。真气人。。

  • 相关阅读:
    Java学习小记 16
    Java学习小记 15
    Java学习小记 14
    Java学习小记 13
    Java学习小记 12
    Java学习小记 11
    Java学习小记 10
    MySql 5.0 以上版本的varchar和text数据类型可以存的汉字个数
    java获取当前上一周、上一月、上一年的时间
    ArtifactsFilter ClassNotFoundException
  • 原文地址:https://www.cnblogs.com/gcfer/p/16531179.html
Copyright © 2020-2023  润新知