1002 Link with Running
题意很简单就是给定一个有向图,每条边两个权值,要求从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;
}
Link with Level Editor II
记得之前在牛客多校上做过这个题的弱化版,那个是直接枚举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...
最后还是带着多年搜索剪枝的功底,把它卡过去了。真气人。。