S2考前综合刷题营Day4
油箱
【问题描述】
有 \(n\) 个城市,每个城市都有加油站,有 \(m\) 条单向道路,距离为 \(x\) 的道路需要消耗 \(x\) 升的汽油。请问你的车辆可以携带的最小油箱容量,使得不限加油次数的情况下,无论你在哪个城市都可以到达任意的城市。
【输入格式】
第一行两个正整数 \(n,m\)。
接下来 \(m\) 行,每行三个正整数 \(x,y,z\) 表示一条 \(x\) 到 \(y\) 的有向边,边权为 \(z\)。
【输出格式】
输出符合条件的最小油箱容量,如果无法满足输出 \(−1\)。
【样例输入】
3 5
1 2 1
1 3 2
2 3 5
3 1 4
2 3 1
【样例输出】
4
【样例解释】
油箱容量为 \(4\) 时,\(1→2,1→3,3→1,2→3\) 道路可以通行,此时满足无论你在哪个城市都可以到达任意的城市。
【数据规模与约定】
\(20\%\) 的数据 \(n≤5,m≤20\)。
\(40\%\) 的数据 \(n≤50m≤200\)。
\(60\%\) 的数据 \(n≤5×10^2,m≤2×10^3\)。
\(100\%\) 的数据 \(n≤5×10^4,m≤2×10^9\)。保证 \(1≤z≤10^9\)。
Solution
100pts
二分答案,问题变成判断一个有向图是否任意两点联通。
相当于判断点 \(1\) 是否能到达所有点并且所有点都能到达点 \(1\)。
可以建立正向图和反向图,并从点 \(1\) 开始 \(dfs\) 遍历。
也可以直接上 \(tarjan\) 来判断是否连通。
时间复杂度 \(O(n\log{n})\)。
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 50010
#define M 200010
using namespace std;
int n,m,x[M],y[M],z[M];
struct edge{int next,to;};
struct Graph
{
int head[N],tot;
bool vis[N];
edge e[M];
void clear()
{
tot=0;
memset(head,0,sizeof(head));
memset(vis,0,sizeof(vis));
}
void add(int u,int v)
{
e[++tot]=(edge){head[u],v};
head[u]=tot;
}
void dfs(int x)
{
if(vis[x])return;
vis[x]=1;
for(int i=head[x];i;i=e[i].next)
dfs(e[i].to);
}
bool check()
{
dfs(1);
for(int i=1;i<=n;i++)
if(!vis[i])
return false;
return true;
}
}E1,E2;
bool check(int cost)
{
E1.clear();
E2.clear();
for(int i=1;i<=m;i++)
if(z[i]<=cost)
{
E1.add(x[i],y[i]);
E2.add(y[i],x[i]);
}
return E1.check()&&E2.check();
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d%d%d",&x[i],&y[i],&z[i]);
int l=1,r=1e9,ans=2e9;
while(l<=r)
{
int mid=l+r>>1;
if(check(mid))ans=mid,r=mid-1;
else l=mid+1;
}
printf("%d\n",ans==2e9?-1:ans);
}
考场上我是用的排序+贪心的思路过的。使图连通的最大边权最小,我们可以很自然的想到最小生成树。
但是最小生成树算法是基于图是没有环的,当若干个点已经连通时,再在它们之间连边显然不优。
所以说,添加一条边的意义一定是使始点的出度或终点的入度减一。我们再加边的时候加上这一步特判即可。
时间复杂度 \(O(n\log{n})\)。
#include<algorithm>
#include<iostream>
#include<cstdio>
using namespace std;
const int N=3e5;
int n,m,ans,cnt;
int in[N],out[N];
struct node
{
int from,to,dis;
}a[N];
bool cmp(node x,node y)
{
return x.dis<y.dis;
}
int main()
{
//freopen("tank_ex.in","r",stdin);
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d%d%d",&a[i].from,&a[i].to,&a[i].dis);
sort(a+1,a+1+m,cmp);
ans=2*n;
for(int i=1;i<=m;i++)
{
int u=a[i].from;
int v=a[i].to;
if(u==v) continue;
if(!in[v]||!out[u])
{
cnt++;
ans-=(in[v]==0)+(out[u]==0);
out[u]++;
in[v]++;
}
if(ans<=0)
{
printf("%d\n",a[i].dis);
return 0;
}
}
printf("-1\n");
return 0;
}
求和
【问题描述】
给定一颗 \(n\) 个点组成的树,根节点为 \(1\) 号节点,每个点上有两个权值 \(a_i, b_i\) 。共有 \(m\) 次询问,每次询问给出两个正整数 \(x, y\),从 \(x\) 到 \(y\) 的路径设为 \(t_1,t_2,…,t_k\)。要求输出
\(\sum_{1≤i<j≤k}a_{t_i}∗b_{t_j}\)。
【输入格式】
第一行两个正整数 \(n,m\)。
第二行 \(n-1\) 个数 \(f_2,f_3,…,f_n\),表示点 \(i\) 的父亲 \((f_i< i)\)。
接下来两行,每行 \(n\) 个数 分别表示 \(a_i,b_i\)。
接下来 \(m\) 行,每行两个正整数 \(x_i,y_i\) ,表示第 \(i\) 次询问。
【输出格式】
对于每个询问操作,输出答案。
【样例输入】
5 4
1 2 3 4
1 2 3 4 5
1 2 3 4 5
1 2
1 3
1 4
1 5
【样例输出】
2
11
35
85
【数据规模与约定】
\(20\% n,m<=100\)。
\(40\% n,m<=2000\)。
另 \(20\%\) 保证 \(a_i=b_i\)。
另 \(20\%\) 保证 \(f_i=i-1, x<=y\)。
\(100\% n,m<=100000\) 保证 \(1<=a_i,b_i<=10000\)。
Solution
20pts
纯暴力,\(O(n^2)\) 求路径贡献。
时间复杂度 \(O(n^2m)\) 。
40pts
我们可以将 \(\sum_{i<j}a_{t_i}*b_{t_j}\) 看作是枚举 \((i,j)\) 这个有序数对,这样的话每次询问可以看作是在 \(1~k\) 区间中找所有的有序数对,然后乘积就和。
考虑如何将 \([1,y]\) 的答案扩展到 \([1,y+1]\)。发现后者的答案比前者多了 \(b_{y+1}*\sum_{1<=i<=y}a_i\),所以我们可以 \(O(1)\) 扩展。
时间复杂度 \(O(nm)\) 。
60pts
式子变成了 \(\sum_{i<j}a_{i}*a_{j}\) 。
发现此时 \(i\) 和 \(j\) 的大小顺序其实不影响,结果都是 \(a_{i}*a_{j}\)。
所以我们可以调换 \(i,j\):\(\sum_{i>j}a_i*a_j\),得到与上式相等的式子。
我们将两式相加:\(2A=\sum_{i!=j}a_i*a_j=\sum_{i,j}a_i*a_j-\sum_{i=j}a_i*a_j=\sum_{i,j}a_i*a_j-\sum_i{a_i}^2\)
我们将式子展开就可以得到:\(A=\frac{(\sum_ia_i)^2-\sum_i(a_i)^2}{2}\)。
所以我们只需要维护路径和和路径平方和即可,利用 LCA 和树上差分 \(O(1)\) 求答案。
时间复杂度 \(O(n\log{n})\) 。
80pts
\(40pts\) 的扩展方法还是太慢,我们需要进行优化。
树上路径是可以分成两段的,现在我们有了两段的答案,考虑怎么求得大区间的答案。
左区间的答案是 \(\sum_{1<=i<j<=t}a_i*b_j\),右区间的答案是 \(\sum_{t<i<j<=x}a_i*b_j\),大区间的答案是:\(\sum_{1<=i<j<=x}a_i*b_j\) 。
还是将这个过程看作是枚举点对,左区间的答案相当于两个点都在左边的答案,右区间的答案相当于两个点都在右边的答案,缺少的正好是一个点在左区间,一个点在右区间的答案 \(\sum_{1<=i<=t<j<=x}a_i*b_j\)。
\(\sum_{1<=i<=t<j<=x}a_i*b_j=\sum_{1<=i<=t}a_i*\sum_{t<j<=x}b_j\)
100pts
将上述做法扩展到树上即可。\(ans[x]+ans[y]-2*ans[lca]\)。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
inline ll read()
{
char ch=getchar();ll a=0;
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') a=a*10+(ch^48),ch=getchar();
return a;
}
const ll N=1e5+5;
ll n,m,Edge;
ll a[N],b[N],head[N],dep[N],f[N][21];
ll Sa[N],Sb[N],Ans1[N],Ans2[N];
struct node
{
ll to,nxt;
}e[N<<1];
void add(ll from,ll to)
{
Edge++;
e[Edge].to=to;
e[Edge].nxt=head[from];
head[from]=Edge;
}
void dfs(ll u,ll fa)
{
dep[u]=dep[fa]+1;
Sa[u]=Sa[fa]+a[u];
Sb[u]=Sb[fa]+b[u];
Ans1[u]=Ans1[fa]+b[u]*Sa[fa]; //Ans1[u]表示1~u的路径的答案
Ans2[u]=Ans2[fa]+a[u]*Sb[fa]; //Ans2[u]表示u~1的路径的答案
for(ll i=head[u];i;i=e[i].nxt)
{
ll v=e[i].to;
dfs(v,u);
}
}
ll LCA(ll x,ll y)
{
if(dep[x]<dep[y]) swap(x,y);
for(ll i=20;i>=0;i--)
{
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
}
if(x==y) return x;
for(ll i=20;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int main()
{
n=read();m=read();
for(ll i=2;i<=n;i++)
{
f[i][0]=read();
add(f[i][0],i);
}
for(ll j=1;j<=20;j++)
for(ll i=1;i<=n;i++)
f[i][j]=f[f[i][j-1]][j-1];
for(ll i=1;i<=n;i++)
{
a[i]=read();
}
for(ll i=1;i<=n;i++)
{
b[i]=read();
}
dfs(1,0);
for(ll i=1;i<=m;i++)
{
ll x=read();
ll y=read();
ll lca=LCA(x,y);
ll ans1=Ans2[x]-Ans2[lca]-(Sa[x]-Sa[lca])*Sb[lca]; //算出x~son[lca]的答案
ll ans2=Ans1[y]-Ans1[f[lca][0]]-Sa[f[lca][0]]*(Sb[y]-Sb[f[lca][0]]); //算出lca~y的答案
printf("%lld\n",ans1+ans2+(Sa[x]-Sa[lca])*(Sb[y]-Sb[f[lca][0]])); //合并两区间
}
return 0;
}
染色
【问题描述】
一排有 \(n\) 个格子,染成 \(m\) 种颜色,相邻格子颜色不能相同。此外,允许一个长度不超过 \(k\) 的区间染成相同的颜色。求最小代价。
【输入格式】
第一行包含三个正整数 \(n, m, k\) 表示格子数量、颜色数量、区间长度。
接下来的 \(n\) 行,每行有 \(m\) 个正整数 \(cost i,j\) 表示将第 \(i\) 个格子染成颜色 \(j\) 需要付出的代价。
由于输入数据过大,可能需要使用快速读入。
inline int read()
{
int x=0;char ch=getchar();
while(ch<'0'|ch>'9')ch= getchar();
while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch= getchar ();
return x;
}
【输出格式】
输出一个整数,表示合法方案的最小代价。
【样例输入】
5 3 3
1 5 6
1 2 3
9 1 9
9 1 9
1 2 3
【样例输出】
6
【样例说明】
颜色分别为 \(1, 2, 2, 2, 1\)。
【数据规模与约定】
对于 \(15\%\) 的数据, \(n, m<=10, k<=n\)
对于 \(40\%\) 的数据, \(n, m<=100, k<=10\)
对于 \(60\%\) 的数据, \(n, m<=300, k<=n\)
对于另 \(15\%\) 的数据, \(n, m<=2000, k=1\)
对于 \(100\%\) 的数据, \(n, m<=2000, k<=n, cost<=10000\)
Solution
40pts
考虑用动态规划来解决。
先考虑 \(k=1\) 的情况:
\(dp[i][j]\) 表示第 \(i\) 的格子染了第 \(j\) 种颜色,前 \(i\) 个格子都染色的最小代价。
\(dp[i][j]=\min(dp[i][j],dp[i-1][j']+cost[i][j])(j≠j')\)
将 \(j'\) 分成小于 \(j\) 和大于 \(j\) 的两部分,所以有:
\(dp[i-1][j']=\min(\min(dp[i-1][j'](j'<j)),\min(dp[i-1][j'](j'>j)))\)。
同时维护 \(dp[i][j]\) 的前缀最小值和后缀最小值。
时间复杂度 \(O(n^2)\)。
60pts
考虑 \(k≠1\) 的情况。
我们可以枚举区间 \([l,r]\) 和颜色 \(t\),枚举量 \(O(nmk)\)。
中间的代价已经固定,剩余变成了 \([1,l-1]\) 和 \([r+1,n]\),并且 \(l,r\) 不能染成 \(t\)。
和 \(k=1\) 的情况相同,所以只需要提前预处理前缀\(dp\) 和后缀\(dp\),\(O(1)\) 得出代价。
时间复杂度 \(O(nmk)\)。
100pts
先枚举颜色 \(t\),将上述做法枚举区间进行优化。
代价为 \(dpL[l-1][t]+Sum[r][t]-Sum[l-1][t]+dpR[r][t]\)。
\(=(dpL[l-1][t] -Sum[l-1][t])+(Sum[r][t]+dpR[r][t])\)。
设 \(A[i]=sum[i][j]+min(gl[i+1][j−1],gr[i+1][j+1]),B[i]=min(fl[i−1][j−1],fr[i−1][j+1])−sum[i−1][j]\)。
然后可以单调队列优化。
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 3005
using namespace std;
int n,m,k,cost[N][N],sum[N][N];
int f[N][N],fl[N][N],fr[N][N];
int g[N][N],gl[N][N],gr[N][N];
int A[N],B[N],q[N],l,r;
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%d",&cost[i][j]);
sum[i][j]=sum[i-1][j]+cost[i][j];
}
memset(f,0x3f,sizeof(f));
memset(fl,0x3f,sizeof(fl));
memset(fr,0x3f,sizeof(fr));
for(int j=1;j<=m;j++)
f[0][j]=fl[0][j]=fr[0][j]=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
f[i][j]=cost[i][j]+min(fl[i-1][j-1],fr[i-1][j+1]);
for(int j=1;j<=m;j++)
fl[i][j]=min(f[i][j],fl[i][j-1]);
for(int j=m;j;j--)
fr[i][j]=min(f[i][j],fr[i][j+1]);
}
memset(g,0x3f,sizeof(g));
memset(gl,0x3f,sizeof(gl));
memset(gr,0x3f,sizeof(gr));
for(int j=1;j<=m;j++)
g[n+1][j]=gl[n+1][j]=gr[n+1][j]=0;
for(int i=n;i;i--)
{
for(int j=1;j<=m;j++)
g[i][j]=cost[i][j]+min(gl[i+1][j-1],gr[i+1][j+1]);
for(int j=1;j<=m;j++)
gl[i][j]=min(g[i][j],gl[i][j-1]);
for(int j=m;j;j--)
gr[i][j]=min(g[i][j],gr[i][j+1]);
}
int ans=1e9;
for(int j=1;j<=m;j++)
{
l=1,r=0;
for(int i=1;i<=n;i++)
{
A[i]=sum[i][j]+min(gl[i+1][j-1],gr[i+1][j+1]);
B[i]=min(fl[i-1][j-1],fr[i-1][j+1])-sum[i-1][j];
}
for(int i=1;i<=n;i++)
{
while(l<=r&&i-q[l]>=k)l++;
while(l<=r&&B[q[r]]>=B[i])r--;
q[++r]=i;
ans=min(ans,A[i]+B[q[l]]);
}
}
printf("%d\n",ans);
}
数字
【问题描述】
给出 \(n\) 个好数和 \(m\) 个坏数,求包含所有好数但不包含任何坏数,并且只由 \(1-4\) 组成的数中,各位数字之和最小值是多少。(好数和坏数均为 \(k\) 位数,且由 \(1-4\) 组成)
【输入格式】
第一行包含三个正整数 \(n, m, k\)
接下来 \(n\) 行包含每行 \(1\) 个正整数,表示好数。
接下来 \(m\) 行包含每行 \(1\) 个正整数,表示坏数。
【输出格式】
输出一个正整数,表示最小的值。输入数据保证一定存在解。
【样例输入】
3 3 4
1111
1222
2333
1122
1133
3122
111
【样例输出】
20
【样例说明】
12223331111
【数据规模与约定】
对于 \(20\%\) 的数据,$ n<=5 ,m=0 ,k=2$
对于 \(30\%\) 的数据,$ n<=10 ,m<=10 ,k<=5$
对于 \(40\%\) 的数据,\(n<=17, m<=20, k<=5\)
对于 \(50\%\) 的数据, \(n<=18, m<=20, k<=5\)
对于 \(60\%\) 的数据, \(n<=19, m<=20, k<=5\)
对于 \(70\%\) 的数据, \(n<=20, m<=20, k<=5\)
对于 \(100\%\) 的数据, \(n<=20, m<=10000 ,k<=8\)
Solution
看成不断添加一个新数字>
这样相当于好数组成一个哈密顿通路,只需要求出两两好数之间最短距离,然后状压 \(dp\) 求出哈密顿通路>
好数之间距离可以使用最短路算法求出,数据范围较小时可能有些其他的求法.
由于边权均为 \(1~4\),可以拆边使用 \(bfs\) 求最短路,直接最短路可能会被卡时间,但是看起来评测机很快。
\(8\) 位 \(1-4\) 组成的数可以看作 \(4\) 进制数进行运算,从而节省空间。
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 100010
#define pa pair<int,int>
using namespace std;
int n,m,k;
int good[25],bad[10005];
int dis[N][4];
bool vis[N];
int trans(int x)
{
int y=0;
for(int i=0;i<k;i++)
{
y|=(x%10-1)<<(2*i);
x/=10;
}
return y;
}
queue<pa>q;
void bfs(int x)
{
memset(dis,0x3f,sizeof(dis));
if(vis[x])return;
dis[x][0]=0;
q.push(pa(x,0));
while(!q.empty())
{
pa x=q.front();
q.pop();
int now=dis[x.first][x.second];
if(x.second)
{
if(dis[x.first][x.second-1]>1e9)
{
dis[x.first][x.second-1]=now+1;
q.push(pa(x.first,x.second-1));
}
continue;
}
for(int i=1;i<=4;i++)
{
int y=((x.first<<2)|(i-1))&((1<<(2*k))-1);
if(dis[y][i-1]>1e9&&!vis[y])
{
dis[y][i-1]=now+1;
q.push(pa(y,i-1));
}
}
}
}
int dist[25][25];
int f[21][1<<20];
int calc(int x)
{
int y=0;
for(int i=0;i<k;i++)
{
y+=x%10;
x/=10;
}
return y;
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%d",&good[i]);
for(int i=1;i<=m;i++)
{
scanf("%d",&bad[i]);
vis[trans(bad[i])]=1;
}
for(int i=1;i<=n;i++)
{
bfs(trans(good[i]));
for(int j=1;j<=n;j++)
dist[i][j]=dis[trans(good[j])][0];
}
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++)
f[i][1<<(i-1)]=calc(good[i]);
for(int S=0;S<(1<<n);S++)
for(int i=1;i<=n;i++)
if((S&(1<<(i-1))))
{
for(int j=1;j<=n;j++)
if(!(S&(1<<(j-1))))
f[j][S|(1<<(j-1))]=min(f[j][S|(1<<(j-1))],f[i][S]+dist[i][j]);
}
int ans=1e9;
for(int i=1;i<=n;i++)
ans=min(ans,f[i][(1<<n)-1]);
printf("%d\n",ans);
}