2024.11.01模拟赛( — _ — )
唉 不——开——心——
有些话就不说了。T1打假了,打了T3、T4的特殊样例(共10分),原本是抱着爆0的心态的,结果没想到T1数据水到直接给了我70分——但T3T4爆掉了, 总分70分 。差点爆0,不——开————心——————
T1【二分图匹配】
题目大意:
给出两个长度分别为n,m(1<=n<=1e3,1<=m<=1e6)的字符串s1,s2,字符串仅由字符'A'~'Z'组成。 翻译一通后: 求s1,s2最长公共子串的长度。
解题思路:
我们都知道,求最长公共子串的dp复杂度为O(nm),我们又知道部分分的写法和正解没啥关系,所以说我们不从最长公共子串入手。
但是,毕竟该题是要求最长公共子串,所以我们仍往动规的方向去思考。我们可以会想到设\(f_{i,j}=k\),表示s1匹配到i、s2匹配到j的最长公共子串长度为k。但是显然,这样的状态转移复杂度仍为O(nm),是没有前途的。我们仔细思考,会想到:\(f_{i,j}=k\leqslant min(n,m)\)恒成立,若是将状态设为\(f_{k,i}=j\),转移的复杂度就变成了\(O(n^2)\),这样是绝对可以过的!
那么开始dp。设状态\(f_{i,j}\)=k表示:LCS长度为i,s1串匹配到j位置,s2串所匹配到的位置。这样,状态就从“答案”变成了“是否存在”。对于每个\(f_{i,j}\),它可以让\(s1_{j}\)作为答案序列的一部分,那么此时
\(f_{i,j}=nxt_{f_{i-1,j-1},s1_{j}}\) (其中\(nxt_{i,j}\)表示在\(s2\)串中,i下标后第一个j元素的位置,\(O(30m)\)可以预处理出);若\(s1_{j}\)不作为答案的一部分,此时\(f_{i,j}=f_{i,j-1}\)。因为当LCS长度相同时,我们要使\(s2\)匹配到的下标最小,所以两者状态取min。
最后,若\(f_{i,n}\)的值合法,即\(f_{i,n}\leqslant m\),说明这个i答案是可以作为答案的,输出即可。
70’究竟是打假的正解还是运气好的骗分?
交程序的时候,我完全就是抱着爆0的心态去交的。在调试时,我给自己出了一组数据:忘了,和忘了。
根据我原本定义的dp状态:dp[i]表示当s1串匹配到第i位时,最优的LCS长度与s2串匹配到的位置。
但这样会出现一个问题:比如说在以上数据中,当i=5时,最优的长度为5、但s2串匹配到了最后一位;
当i=?时,按理来说它应通过i=5不那么优的一个方案转移而来,但因为我的dp数组少了一维状态而并不是最优的方案。
但神奇的是,交上去的了70。当时很多人都想知道我是怎么在自己出的数据和大数据都没过的情况下骗到这么多分的,
但那时我也说不清楚,以为是纯纯数据水才让我的错误代码骗到了分。
现在想来,我的dp与正解的dp类似,但相比少了一维长度状态,所以有时就无法取到最优解;而其他的转移也类似,
比如正解中的nxt数组和我程序中的box实现的功能相同。
所以,我的70分并不是的单纯的数据水。我看似全错的dp转移也只是比正解少了一维状态。哎,又是与正解擦肩而过的一次。
可达性dp 神奇的小东西
一大坨代码
#incIude <bits/stdc++.h>
using namespace std;
const int N=2e3+10;
const int M=1e6+5;
int n,m;
int a[M],b[M],box[100];
int f[N][N];//f[i][j]:匹配i个、s1匹配到下标j,s2所匹配到的地方
int nxt[M][30];//nxt[i][j]:b[i]后第一个值为j的位置
string s1,s2;
int ans;
void init()
{
memset(box,0x3f,sizeof box);
memset(nxt,0x3f,sizeof nxt);
}
int main()
{
init();
cin>>n>>m>>s1>>s2;
for (int i=0;i<n;i++) a[i+1]=(int)(s1[i]-'A');
for (int i=0;i<m;i++) b[i+1]=(int)(s2[i]-'A');
for (int i=m;i>=0;i--)//倒序枚举,box记录上一个j值所出现的位置
{
for (int j=0;j<=25;j++)
{
nxt[i][j]=box[j];
if (b[i]==j) box[j]=i;
}
}
for (int i=1;i<=n;i++) f[i][0]=m+1;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
f[i][j]=min(f[i][j-1],nxt[f[i-1][j-1]][a[j]]);//不匹配第j个,与匹配第j个
}
}
for (int i=n;i>=1;i--)
{
if (f[i][n]<=m)//没有跳出去 即可行
{
cout<<i;
break;
}
}
return 0;
}
T2【虚图】
题目大意:
给出由n个节点、m条边组成的无向图(1<=n,m<=2e5),给出T个“关键点”,求任意两个关键点距离的最小值。
解题思路:
这题乍一看,是道既不能n下单源最短路、也不能多源最短路的神奇题目。但是我们想到,伟大的dijstra算法复杂度为\(O(nlogn)\),要是能想办法跑个dij,那我们岂不是就可以随便吊打复杂度了?
但是众所周知,dij是单源最短路;又但是,我们注意到,在该题目中只有关键点有用,那么我们或许会想起双向搜索——走向终点的同时,终点也在走向我,那么,这道题可不可以也用类似的东西捏……
于是乎!我们联想到了把所有的“起点”与“终点”放进小根堆中,也就放进所有的关键点,让它们一起跑dijstra。接着,我们就神奇地发现:没问题了!对于一个点,若它被两个关键点的路径松弛过,那么它一定会是一个答案(因为是dij,它一定会是优的)。但是!我们要注意一下,“两个关键点”,这很重要,每个\(dis_{i}\)都需要所对应的\(c_{i}\)来记录它是从哪个点走来的。
于是乎,我们把所有的关键点放入大根堆,然后跑一遍普通的dij就好了。但是,我们怎么才能记录答案呢?毕竟对于一个点,我们只记录了一条从某点到它的最小路。我们可以想到,一条边一定连着两个点u,v,若是\(c_{u}\ne c_{v}\),则一定会有一条\(c_{u},c_{v}\)之间的最小路径。所以我们枚举每条边,若是这条边连着两个不同\(c_{i}\)的点,那么就更新下\(ans=min(ans,dis_{u}+dis_{v}+w)\)(这才是一条合法的路径!)。
如果大山不会走向穆罕默德,穆罕默德可以走向大山。
两大坨代码
#incIude <bits/stdc++.h>
#define pii pair<int,int>
#define int long long
using namespace std;
const int N=2e5+5;
int n,m,T;
struct node { int nxt,val; };
vector <node> e[N];
struct node2 { int uu,vv,ww; } b[N];
int p[N];
int ans=0x3f3f3f3f3f3f3f3f;
int dis[N],c[N];
bool vis[N];
priority_queue < pii,vector <pii>,greater<pii> > q;
void read()
{
scanf("%lld%lld%lld",&n,&m,&T);
for (int i=1;i<=m;i++)
{
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
e[u].push_back({v,w});
e[v].push_back({u,w});
b[i]={u,v,w};//记录下边
}
for (int i=1;i<=T;i++) scanf("%lld",&p[i]);
}
void dijstra()
{
memset(dis,0x3f,sizeof dis);
for (int i=1;i<=T;i++)
{
q.push(make_pair(0,p[i]));
dis[p[i]]=0;
c[p[i]]=p[i];//记录是从哪个关键点到的
}
while (!q.empty())
{
pii t=q.top();
q.pop();
int u=t.second;
if (vis[u]) continue;
vis[u]=true;
for (int i=0;i<e[u].size();i++)
{
int v=e[u][i].nxt,w=e[u][i].val;
if (dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
c[v]=c[u];
q.push(make_pair(dis[v],v));
}
}
}
}
signed main()
{
read();
dijstra();
for (int i=1;i<=m;i++)
{
int u=b[i].uu,v=b[i].vv,w=b[i].ww;
if (c[u]==0||c[v]==0) continue;
if (c[u]!=c[v]) ans=min(ans,dis[u]+dis[v]+w);//若是出现u->v->u的情况,那么不要取
}
printf("%lld",ans);
return 0;
}