2024 CSP-NOIP 模拟赛记录
10.29
请自动播放可莉音频 今天照常7:45开始打模拟赛,11:45时结束。打了T1的40分暴力、T3的20分暴力,没有注意到T4的特殊样例可以骗分(悲),最后以60分收尾。总结一下,没有挂分,但也没和正解挨上边,算是不好也不坏吧。
订题时我看着T1 26行的AC代码陷入了沉思。三个人,想了至少三个小时,结果全没想出来,于是来整理一下今天的神奇模拟赛。
T1【集合】
题目大意:
给定一棵\(n\)个节点的树,共\(m\)次操作,每次操作形如对于第\(x\)条边\((u,v)\),将\(S_{u},S_{v}\)替换成两者的并集。求\(m\)次操作后,每个节点\(i\)属于多少个集合
解题思路:
经过一定的思考之后,我们发现正序操作可以理解为“我会有多少个朋友”,明显不好处理;但倒序操作可以理解为“我会是谁的朋友”,可以转移。
举个栗子:对于操作加盟\(u\)->\(v\)边,此时\(u\)与\(v\)中都包含彼此,所以当下次对\(u\)进行操作时,\(v\)一定也会被那个节点包含。
可以转移为\(s_{u}=s_{v}=s_{u}+s_{v}\)。但这样当出现重边时,因为u与v已有相同元素,这个操作就会应取了相同元素而不满足集合性质(题目中取并集)。但是,众所周知,\(|S∪T|=|S|+|T|−|S∩T|\),而经过一系列推论我们容易发现\(|S∩T|\)为上一次对该边操作后的答案,即上一次操作取的并集会是此次操作中\(u\)与\(v\)的交集。那么会不会出现\(|S∩T|\)中不仅仅是上次并集中的元素情况呢?手模后发现是不会的。因为……反正……这样这样,那样那样,因为树上两点的简单路径是唯一的,若\(S、T\)中的其他元素想要影响\(|S∩T|\),只能通过再次操作该边影响,所以上次的操作的集合并就是这次操作中的集合交。
于是乎,我们维护\(pre\)数组与\(ans\)数组,\(pre_{i}\)表示对于边\(i\),上一次操作的答案,\(ans_{i}\)表示对于节点\(i\)之中元素的个数。于是我们稍加整理,可以得到神奇的式子:\(ans_{u}=ans_{v}=pre_{op_{i}}=ans_{u}+ans_{v}-pre_{op_{i}}\)。于是26行代码就解决了
想过3小时思路不对,没想过思路完全不着边。
代码
#incIude <bits/stdc++.h>
using namespace std;
const int N=4e5+10;
int n,m;
int u[N],v[N];//记录每条边
int op[N];//操作
int ans[N],pre[N];//答案与上次答案
void read()
{
scanf("%d%d",&n,&m);
for (int i=1;i<n;i++) scanf("%d%d",&u[i],&v[i]);
for (int i=1;i<=m;i++) scanf("%d",&op[i]);
}
int main()
{
read();
for (int i=1;i<=n;i++) ans[i]=1;
for (int i=m;i>=1;i--)
{
int uu=u[op[i]],vv=v[op[i]];
ans[uu]=ans[vv]=pre[op[i]]=ans[uu]+ans[vv]-pre[op[i]];
}
for (int i=1;i<=n;i++) printf("%d ",ans[i]);
return 0;
}
10.31
一定要好好睡觉啊,不然打模拟赛的时候会困死的!!!
非常非常困的7:50时就开始打模拟赛,还是打了四个小时。打了T1、T2的正解,T3的5分特殊样例、T4的10分特殊样例,预计总215分。
然后经过漫长的三个小时的等待,出现了 T1 100分,T2 65分,T3 60分,T4 10分、总分235分 的神奇成绩。虽然结果比预估的高,但我的T2挂了整整35分!这让我非常的不爽。当我发现是因为没开long long时,不开心的心情到达了顶峰————不然我就可以排名第二了!!
T1【恋曲】
题目大意:
给出n,t,x,与长度为n(1<=n<=1e6)的两个序列a,b。每次操作可以使\(c_{i}=min(c_{i}+b_{i},a_{i})\),最多操作t(1<=t<=1e9)次,问是否可以使\(\Sigma c_{i}>x\)(\(c_{i}\)初始化为0)
解题思路:
感觉可以贪心。每次操作花费代价一定,所以能取大的\(b_{i}\)就取。于是乎,想到用大根堆维护最大值,类型为pair<int,int>,第一个为\(b_{i}\),第二个为可使用的次数。我们注意到,对于每个\(b_{i}\)可以分为两个部分,一部分是每次取\(b_{i}\),一共可取\(\lfloor\frac{a_{i}}{b_{i}}\rfloor\)次;另一部分是取\(a_{i}\)%\(b_{i}\),只可以取一次(总和\(a_{i}\)可以分成大小为\(b_{i}\)的若干整份,但总会剩下分不成\(b_{i}\)的一份),在输入时预处理为两部分、插入大根堆即可。
完了后每次取堆顶,然后操作,判断操作次数与总和的边界输出即可。
代码
#incIude <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,t,x;
int a[N],b[N];
long long sum;
long long tol;
priority_queue < pair<int,int> > q;
void read()
{
scanf("%d%d%d",&n,&t,&x);
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
sum+=a[i];
}
for (int i=1;i<=n;i++)
{
scanf("%d",&b[i]);
if (b[i]>a[i]) b[i]=a[i];//防止溢出
q.push(make_pair(b[i],a[i]/b[i]));
if (a[i]%b[i]) q.push(make_pair(a[i]%b[i],1));
}
}
int main()
{
read();
if (sum<x)//就算全部取到也不够
{
printf("No");
return 0;
}
while (!q.empty())
{
int bb=q.top().first,dd=q.top().second;
q.pop();
tol+=min(dd,t)*bb;
t-=min(t,dd);
if (t==0)//边界
{
if (tol>=x) printf("Yes");
else printf("No");
break;
}
if (tol>=x)//边界
{
printf("Yes");
break;
}
}
return 0;
}
T2【留校丕】
题目大意:
给出一个长度为\(n(2\leqslant n\leqslant 10^{6})\)的序列p(保证p是排列),每次操作可以交换p中相邻的两个数,求使p变成单峰的最小操作次数
解题思路:
本来想的p中最大值一定是会是那个“峰”,但显然看最大值很不好操作。我们想到p中的最小值一定需要在序列两边;又因为每次操作只能交换相邻的两个数,所以移动最小值后序列中的其他数不变(比如序列3 1 2 4 5,不论是将1移到序列左边或是右边,3 2 4 5的序列位置都没有改变)。
于是乎,可以把移动mn看作删除mn(显而易见,mn是指当前序列中的最小值),那么每次移动一定要往移动次数少的一边移动,这样的贪心才可使ans最小。但是因为“删除mn”的操作,序列中数的个数会发生改变,也就是说我们要对这个序列进行单点修改与区间查询,一下子就想到了树状数组。但是,作为一个大蒟蒻,我只能写一个线段树来(还好没被卡)。
但!是!不!要!忘!记!ans!要!开!long!long!
记录一下模拟赛时的各种若至小错误
虚空调试的第一步,题意看错,怒失半小时思考时间
虚空调试的第二步,简单问题复杂化,明明从小到大枚举就行,却仍想着用线段树维护区间最小值,怒失20分钟思考时间
虚空调试的第三步,忘记特判边界,本以为是l==r的问题,结果是l>r的问题,怒调20分钟
于是乎,时间所剩不多、没来得及注意细节,ans忘记开long long
再于是乎,痛失T4的打暴力时间
代码
#incIude <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int n;
int p[N],t[N];
int pos[N];
int sum[N*4];
long long ans;
void read()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++)
{
scanf("%lld",&p[i]);
pos[p[i]]=i;//记录一下每个值的位置
}
}
void pushup(int id)
{
sum[id]=sum[id*2]+sum[id*2+1];
}
void build(int id,int l,int r)
{
if (l==r)
{
sum[id]=1;
return ;
}
int mid=(l+r)/2;
build(id*2,l,mid);
build(id*2+1,mid+1,r);
pushup(id);
}
int query2(int id,int l,int r,int i,int j)
{
if (j==0||i==n+1) return 0;//特判目前最小值已在序列边界 不然会死循环
if (l>=i&&r<=j) return sum[id];
int mid=(l+r)/2,sm=0;
if (i<=mid) sm+=query2(id*2,l,mid,i,j);
if (mid+1<=j) sm+=query2(id*2+1,mid+1,r,i,j);
return sm;
}
void del(int id,int l,int r,int x)
{
if (x==l&&x==r)
{
sum[id]-=1;
return ;
}
int mid=(l+r)/2;
if (x<=mid) del(id*2,l,mid,x);
if (mid+1<=x) del(id*2+1,mid+1,r,x);
pushup(id);
}
signed main()
{
read();
build(1,1,n);//建树
int mn=0;//序列当前最小值
for (int i=1;i<=n;i++)
{
mn++;
ans+=min(query2(1,1,n,1,pos[mn]-1),query2(1,1,n,pos[mn]+1,n));//取移到最左边、最右边的更小值
del(1,1,n,pos[mn]);
}
printf("%lld",ans);
return 0;
}
T3【移球游戏】
题目大意:
给定n个长度为m的区间\((1\leqslant n,m\leqslant400)\),在n*m+1的位置有一个空位,每次操作可以将任意区间的一个数移到空位,求使每个区间内的数都不相同的操作数与方案(也就是说,当操作完成后,每个区间都应由1,2,…,m-1,m组成)。
解题思路:
我们可以感性地想象到, 根据数字守恒定律, 在一个区间中多出的值一定会是另一个区间中少的值。进而,我们可以想象到,我们肯定是先从一个区间里拎出来多的值,再塞到另一个少这个值的区间中。那么我们该怎样实现呢?
我们可以:对于一个区间,从它少的值向该区间连边,再从该区间向它多的值连边, 因为多的值也会是少的值,所以 跑一遍这个图就相当于我们上方提到的操作。跑完后我们会发现,它一定会形成环 因为数字守恒定律 ;又因为我们需要把所有的边都跑完,所以这些个环会形成欧拉回路。
当跑完所有的欧拉回路时,这n个区间就一定都合法了。此时,ans1就是欧拉回路的总长度加上欧拉回路的个数(因为每对一个欧拉回路操作,都要先拎出一个值到空位上才能一个个地变),方案就是跑欧拉回路的过程(需要注意图内节点有区间也有点,不要把区间输出来了)。
代码
#incIude <bits/stdc++.h>
using namespace std;
const int N=405,M=160000;
int n,m;
int fa[N*2];
vector <int> box[N*2][N*2];//桶数组
vector <int> e[N*2];//图
int d[N*2];//度数
int id[N*2];//欧拉回路每条边只能走一遍,走过这条就要走下一条了
int w[N*2][N*2];
int tol;
struct node { int x,y; } ans[N*N];
void read()
{
int a;
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
{
for (int j=1;j<=m;j++)
{
scanf("%d",&a);
box[i][a].push_back((i-1)*m+j);
}
}
}
int s[M*2],top;
void build_G()//建图
{
for (int i=1;i<=n;i++)
{
for (int j=1;j<=m;j++)
{
if (box[i][j].size()==0) //少的数
{
e[n+j].push_back(i);
d[n+j]++;
}
else
{
for (int k=0;k<box[i][j].size()-1;k++)//多的个数
{
e[i].push_back(n+j);
d[i]++;
}
}
}
}
}
void dfs(int x)
{
for (;id[x]<e[x].size();)
{
int v=e[x][id[x]++];
d[x]--;
dfs(v);
}
s[++top]=x;
}
int main()
{
read();
build_G();
for (int i=1;i<=n+m;i++)//点
{
if (d[i])//一个没走过的欧拉回路
{
int space=n*m+1;
top=0;
dfs(i);
for (int j=3;j<=top;j+=2)//在栈中,点、区间交错放入 偶为点,奇为区间
{
ans[++tol]={box[s[j]][s[j-1]-n][w[s[j]][s[j-1]-n]],space};
space=ans[tol].x;
w[s[j]][s[j-1]-n]++;//为什么要有w?因为一个区间里可能会多出很多个相同的数,每个数都要拿出去,拿完一个就要拿下一个了
}
ans[++tol]={n*m+1,space};//走完一个回路后要把拿出来的数再放回去
}
}
printf("%d\n",tol);
for (int i=1;i<=tol;i++) printf("%d %d\n",ans[i].x,ans[i].y);
return 0;
}
11.1
有些话就不说了。T1打假了,打了T3、T4的特殊样例(共10分),原本是抱着爆0的心态的,结果没想到T1数据水到直接给了我70分——但T3T4爆掉了, 总分70分 。差点爆0,不——开————心——————
T1【二分图匹配】
题目大意:
给出两个长度分别为\(n,m\ (1\leqslant n\leqslant 10^3,1\leqslant m\leqslant 10^6)\) 的字符串\(s1,s2\),字符串仅由字符\('A'\)~\('Z'\)组成。 翻译一通后: 求 \(s1,s2\) 最长公共子序列的长度。
解题思路:
现在来看,这是 \(LCS\) 模板。
我们会想到设\(f_{i,j}\)表示\(s1\)匹配到\(i\)、\(s2\) 匹配到 \(j\) 的最长公共子串长度。但是,这样的状态转移复杂度为\(O(nm)\),是没有前途的。我们仔细思考,会想到:\(f_{i,j}\leqslant min(n,m)\)恒成立,若是将状态设为\(f_{k,i}=j\),转移的复杂度就变成了\(O(n^2)\),这样是绝对可以过的!
那么开始dp。设状态\(f_{i,j}\) 表示: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\),说明这个答案是 \(n\) 可以作为答案的,输出即可。
70’究竟是打假的正解还是运气好的骗分?
交程序的时候,我完全就是抱着爆0的心态去交的。在调试时,我给自己出了一组数据:忘了,和忘了。
根据我原本定义的dp状态:dp[i]表示当s1串匹配到第i位时,最优的LCS长度与s2串匹配到的位置。
但这样会出现一个问题:比如说在以上数据中,当i=5时,最优的长度为5、但s2串匹配到了最后一位;当i=?时,按理来说它应通过i=5不那么优的一个方案转移而来,但因为我的dp数组少了一维状态而并不是最优的方案。
但神奇的是,交上去的了70。当时很多人都想知道我是怎么在自己出的数据和大数据都没过的情况下骗到这么多分的,但那时我也说不清楚,以为是纯纯数据水才让我的错误代码骗到了分。
现在想来,我的dp与正解的dp类似,但相比少了一维长度状态,所以有时就无法取到最优解;而其他的转移也类似,比如正解中的nxt数组和我程序中的box实现的功能相同。
所以,我的70分并不是的单纯的数据水。我看似全错的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\leqslant n,m\leqslant 2e5)\),给出T个“关键点”,求任意两个关键点距离的最小值。
解题思路:
这题乍一看,是道既不能n下单源最短路、也不能多源最短路的神奇题目。但是我们想到,伟大的dijstra算法复杂度为\(O(n\ logn)\),要是能想办法跑个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;
}
11.2
钢哥说,大家要休息好,于是模拟赛晚点,变成了3小时3道题。T1打的正解(但没调出来版),T2T3打的暴力(但全挂了版),预计总分120+,但实际总分80。
总结一下:昨晚多睡了一小时,今天思路确实感觉更清晰了(但也有可能是因为题目不难……)。但今天时间没分配好。我照常第一个小时把所有题看一遍,深入思考T1+打后两题暴力,两眼一抬,一个小时再次没了。若是T3T4暴力分(期望55)都拿到了那还好,但是全挂了,相当于白白浪费一小时。接着两个小时,忘了咋分配的了,好像还分了点时间又去搞T2,最后大概只有一个多小时的时间去写、去调T1。外加今日T1代码尤其长,这一个小时是极其紧张的————然后就成功挂掉20分。
所以,下次一定要在40分钟内打完后面大题的暴力并思考T1T2,每做完一道题出去换换脑子,回来全神贯注做手上的题,不要再去想刚刚打完的暴力。同时,思考时一定要严谨!科学!准确!模拟赛时没有多少时间来让我调各种纸张错误,只能在敲代码的时候就注意细节、边界等等。我的代码能力实在是难以信任。
T1【秋秋飞车】
题目大意:
忘了 给出一个n个坐标为(x,y)、价值为w的点(1<=n,x,y<=2e5),接下来给出q个操作,每次操作给出方向d与距离v,每次移动可以获得路径上的价值和(包括终点不包括起点),问q次操作后的价值和(1<=n,x,y,v<=2e5)
解题思路:
一眼搜索。看到复杂度后,一眼不是搜索。又发现每次是要路径上的和,每次路径不是横着就是纵着,于是想到每一列、每一行前缀和,因为炸裂的复杂度而用vector存储。然后没了。然后炸了。
代码
#incIude <bits/stdc++.h>
#define int long long
const int N=2e5+10;
using namespace std;
int n,q;
struct NODE { int x,y,w; }a[N];//记录点
struct node { int x,w; };//x值相当于普通前缀和数组的第二维
vector <node> sum1[N],sum2[N];//记录前缀和
int sx=1,sy=1;//看好起始点
int dx[4]={1,0,-1,0},dy[4]={0,1,0,-1};//方向不要和题意搞反
int ans;
void read()
{
scanf("%lld%lld",&n,&q);
for (int i=1;i<=n;i++)
{
int x,y,w;
scanf("%lld%lld%lld",&a[i].x,&a[i].y,&a[i].w);
}
}
bool cmp1(NODE aa,NODE bb)
{
if (aa.x!=bb.x) return aa.x<bb.x;
return aa.y<bb.y;
}
bool cmp2(NODE aa,NODE bb)
{
if (aa.y!=bb.y) return aa.y<bb.y;
return aa.x<bb.x;
}
void init()//搞前缀和
{
sort(a+1,a+1+n,cmp1);
for (int i=1;i<=n;i++)
{
int x=a[i].x,y=a[i].y,w=a[i].w;
if (!sum1[x].size()) sum1[x].push_back((node){0,0});//防止越界
int sum=sum1[x][sum1[x].size()-1].w;
sum1[x].push_back((node){y,sum+w});
}
sort(a+1,a+1+n,cmp2);
for (int i=1;i<=n;i++)
{
int x=a[i].x,y=a[i].y,w=a[i].w;
if (!sum2[y].size()) sum2[y].push_back((node){0,0});
int sum=sum2[y][sum2[y].size()-1].w;
sum2[y].push_back((node){x,sum+w});
}
}
int fd(int x,int y,int k)//不会lower_bound,于是手搓 求sum[x]中第一个<=y的值的下标(k表前缀和类型)
{
if (k==0)
{
int l=0,r=sum1[x].size()-1;
while (l<r)
{
int mid=(l+r+1)/2;
if (sum1[x][mid].x<=y) l=mid;
else r=mid-1;
}
return r;
}
if (k==1)
{
int l=0,r=sum2[x].size()-1;
while (l<r)
{
int mid=(l+r+1)/2;
if (sum2[x][mid].x<=y) l=mid;
else r=mid-1;
}
return r;
}
}
int _abs(int aa)//手搓abs
{
if (aa>=0) return aa;
return -aa;
}
signed main()
{
read();
init();
while (q--)
{
int d,v;
scanf("%lld%lld",&d,&v);
int xx=sx+dx[d]*v,yy=sy+dy[d]*v;
if (d%2==1)//x上不变,y上前缀和
{
if (!sum1[xx].size())
{
sx=xx;
sy=yy;
continue;
}
int d1,d2;
if (yy<sy) d1=fd(xx,yy-1,0),d2=fd(xx,sy-1,0);//因为有“取终点不取起点”的限制,手模后发现不同走法前缀和取法还不一样
else d1=fd(xx,yy,0),d2=fd(xx,sy,0);
ans+=_abs(sum1[xx][d1].w-sum1[xx][d2].w);
}
if (d%2==0)//与上相同
{
if (!sum2[yy].size())
{
sx=xx;
sy=yy;
continue;
}
int d1,d2;
if (xx<sx) d1=fd(yy,xx-1,1),d2=fd(yy,sx-1,1);
else d1=fd(yy,xx,1),d2=fd(yy,sx,1);
ans+=_abs(sum2[yy][d1].w-sum2[yy][d2].w);
}
sx=xx;
sy=yy;
}
printf("%lld",ans);
return 0;
}
一切的一切都没问题,一开始我的想法没错,过程中也没有出关键性的大错误,但仍然在细节处理上出了岔劈。但要是跑样例出错时,我像我下午那样分别手模两种情况,我也不会没调出来的。只因调程序时只剩下了不到半小时,调程序时我完全无法沉下心来去想。
所以说,还是两点,一是时间规划不合理,二是细节处理不到位(老问题了)。所以说,还是得保证睡眠以保证清醒的脑子与思路,有了清醒的脑子才能全神贯注地去想题、去处理细节。感觉代码能力有提高,不管程度怎样,也算是一件好事。
T2T3不会不会,不写不写(题解怎么写的那么抽象啊——)
11.4
模拟赛没怎么好好打。第二次打构造题,这种题和其他的是真的不一样。看来我不仅要恶补各种算法,还要锻炼一下我的思维。
T1【串】
题目大意:
给出n,k(1<=n,k<=1e5),要求构造出长度为n的01串,其中1出现k次,使得“1”出现奇数次的区间数量最多。求最多的区间个数,与构造方案
解题思路:
一眼构造->一眼不会->认认真真手算了很久,结果和上次一样从头假到尾。
根本不会,于是乎只能等题解。
————
将答案数组a[n]做前缀和,容易发现,每当1出现一次,前缀和数组sum的奇偶性就会变一次。若是要“1”要在区间(l,r)出现奇数次,那么sum[r]-sum[l-1]也是奇数。
于是乎,将前缀和数组中的奇数个数统计为x,偶数个数记为y,则有x+y=n-1,我们要使ans=xy最大,即要让x,y尽量接近,这容易算出x,y。然后,我们怎么构造呢?我们只需要使数列的前k-1位都为1、根据奇偶性在数组中放入最优的剩下的“1”就行。
点击查看代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,k;
int sum[N];
int x,y;
signed main()
{
cin>>n>>k;
if (k==0)
{
cout<<0;
return 0;
}
x=y=(n+1)/2;
if (n%2==0) y++;
cout<<x*y<<endl;
x--;
for (int i=1;i<=k-1;i++)
{
cout<<"1 ";
sum[i]=sum[i-1]+1;
if (sum[i]%2==0) x--;
else y--;
}
for (int i=k;i<=n;i++)
{
if (x==0||y==0)
{
cout<<"1 ";
for (int j=i+1;j<=n;j++) cout<<"0 ";
break;
}
cout<<"0 ";
sum[i]=sum[i-1];
if (sum[i]%2==0) x--;
else y--;
}
return 0;
}
11.5
然后呢,早上困得要死。困到什么地步了呢?困到T3T4读下来之后就全都忘了。是一点都不记得了那种。本来想着去厕所睡会儿,结果被那里的味道和冷空气劝退了。回到教室之后,才慢慢地清醒了过来,仔细推了下T1,又打了T2的暴力,T3T4实在不知道怎么写,就没有写。没有挂分,这是一件非常好的事情,T1 100分,T2 20分,共120分,开开心心开始订T2。
T1【同色三角形】
题目大意:
给定一张有n(1<=n<=1e5)个节点的无向完全图,给出m(1<=m<=3e5)条白边,剩下的边为黑边,求同色的三(三)元(角)环(形)的个数。
解题思路:
小小地运用一些排列组合的方法,可以求出这个图中所有边的数量。我们可以想到:若有一条边变成了白边,那么原本以这条白边组成的三角形就不合法了。每条边可以组成n-2个不同的三角形,所以所有白边的出现共会导致m*(n-2)个三角形不合法。
但是!若一个三角形有两条白边,它理应只被更新一次,却被两个白边更新了两次。这时候,我们就容斥一下,对于每个点,如果它的度数超过一(即出现上述情况),那么就要把每个三角形多减的加回来,也就是d[i]*(d[i]-1)/2(很好理解对叭)。
那要是一个三角形全是白边呢?我们可以想象:这个三角形第一次被减了3,第二次又加回了3(因为它会被每一条边都更新一遍),此时就相当于它仍被记了一个数,这正是我们想要的,所以就不用管。
代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m;
int d[N];
int tol;
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1;i<=m;i++)
{
int u,v;
scanf("%lld%lld",&u,&v);
d[u]++;
d[v]++;
}
tol=n*(n-1)*(n-2)/6;
tol-=(n-2)*m;
for (int i=1;i<=n;i++)
{
if (d[i]>1) tol+=d[i]*(d[i]-1)/2;
}
printf("%lld",tol);
return 0;
}
T2T3T4没有讲,题解也看不懂,不会不会
11.14
四个小时的模拟赛,四道小清新计数题。前面时间状态还是不太好,一个多小时把T1搞出来了,T2T3T4都不会,暴力也没打成。
T1又顺便挂了20分,所以最后总分只有80。是得好好调整状态了。
T1【字符串】
题目大意:
给出两个字符串\(s,t\),要求求出所有\(x\)的个数,使得\(x=p+q\)(\(p,q\)分别是\(s\)串的非空前缀、\(t\)串的非空后缀)
设\(n=|s|,m=|t|,1\leqslant n,m\leqslant 10^{6}\)。
解题思路:
所有\(x\)的个数即为\(n\times m\),但由于\(s,t\)串中重复的字符拼接时会导致相同的\(x\)被统计两次,所以需要统计字符串中相同的字符后去重,总数减去一个字符在\(s\)串出现的次数乘在\(t\)串中出现的次数即可。
但!注意到前后缀非空,所以统计字符时要跳过\(s_{1}\)与\(t_{m}\)。
挂分小技巧:取模出错。
代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
const int MOD=998244353;
char s1[N],s2[N];
int a1[N],a2[N];
int cnt1,cnt2;
int bx1[N],bx2[N];
int ans;
signed main()
{
scanf("%s%s",s1,s2);
for (int i=0;s1[i]!='\0';i++) a1[++cnt1]=(int)(s1[i]-'a');//闲的
for (int i=0;s2[i]!='\0';i++) a2[++cnt2]=(int)(s2[i]-'a');
ans=(cnt1*cnt2)%MOD;
for (int i=2;i<=cnt1;i++) bx1[a1[i]]++;
for (int i=1;i<cnt2;i++) bx2[a2[i]]++;
for (int i=0;i<26;i++) ans=(ans-(bx1[i]*bx2[i])%MOD+MOD)%MOD;//注!意!取!模!
printf("%lld",ans);
return 0;
}
11.15
抽象的题目。不多做评价了。
打了T1的正解,T2的“暴力”,预计总分140。但T1没注意到\(\Sigma n\)为\(10^{6}\),多测初始化初始炸了;T2数据太水,导致暴力完全能过,但是因特判特殊数据特判的不对所以炸了,实际上T1 60分,T2 80分,总分140分。一切的一切都说得过去,但是……挂了整整60分!!再次与第二失之交臂!!!
T1【最优排序】
题目大意:
给出一个值域为\([1,n]\)的排列\(p\),每次操作可以选取至多四个互不相同的位置,并任意交换他们的位置。求最少操作几次可以使\(p\)升序排列。\((1\leqslant T,n,\Sigma n\leqslant 10^{6})\)
解题思路:
看到这道题又手模了一会儿,突然联想到之前模拟赛的T3,于是就开始非常激动地建图……统计……然后炸了。
我们可以将不好的\(p_{i}\)指向\(i\),我们可以想到一定存在若干个\(p_{i}\),交换他们之间的位置后一定合法,手模后发现这些\(p_{i}\)会形成一个环。于是乎,我们可以建图后统计出所有环的节点个数以计算答案。
对于一个节点数量为\(t_{i}\)的环,我们可以先取到相邻4个节点并进行交换,我们容易发现一次操作会产生一个不合法的\(p_{i}\)与三个合法的\(p_{j}\),那么下一次操作包含上次不合法的\(p_{i}\)与3个新的\(p_{j}\)即可,重复以上操作就可以使整个环合法。
所以,对于该环就要操作\(\lceil\frac {t_{i}}{3}\rceil\)次。但是!可以想到,要是最后只用操作两个点的话,那么这次操作可以和其他环一起操作,最后答案即\(\Sigma^{m}_{i=1}{\lfloor\frac{t_{i}}{3}\rfloor}+\lceil\frac{\Sigma^{m}_{i=1}[t_{i} mod 3 =2]}{2}\rceil\)。
因为这题情况非常简单,所以根本不用跑\(Tarjan\),跑个\(dfs\)就行。且,初始化补药\(memset\),重复到\(n\)就行,不然会炸。
代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int T;
int n;
int p[N];
vector <int> e[N];
int vis[N];
int ans,tol;
void init()
{
for (int i=0;i<=n;i++) e[i].clear();
for (int i=0;i<=n;i++) vis[i]=0;
ans=tol=0;
}
int dfs(int x)
{
vis[x]=1;
int res=1;
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (vis[v]) continue;
res+=dfs(v);
}
return res;
}
signed main()
{
scanf("%lld",&T);
while (T--)
{
init();
scanf("%lld",&n);
for (int i=1;i<=n;i++)
{
scanf("%lld",&p[i]);
if (p[i]==i) continue;
e[i].push_back(p[i]);
}
for (int i=1;i<=n;i++)
{
if (!vis[i]&&p[i]!=i)
{
int cnt=dfs(i);
ans+=cnt/3;
if (cnt%3==2) tol++;
}
}
printf("%lld\n",ans+(tol+1)/2);
}
return 0;
}
T2【图上移动】
题目大意:
给出一个\(n\)个节点、\(m\)条边的无向图,可以给每个点赋值,要求找到一条最长的路径,使得每次移动都是向当前节点相邻权值最小的节点移动。\((1\leqslant n\leqslant 40)\)
解题思路:
爆搜。期望得分100,特判失败,实际得分80。
容易发现,我们的路径上不能走完一个环,否则无法达到要求。所以直接枚举起点\(dfs\),若与当前节点相连的节点曾经走过的话,那么当前节点不可走,直接return回去即可。
代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=50;
int n,m;
vector <int> e[N];
int ans;
int vis[N];
void dfs(int x,int _fa,int tol)
{
vis[x]=vis[_fa]+1;
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (v!=_fa&&vis[v]) return ;
}
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (v==_fa) continue;
dfs(v,x,tol+1);
vis[v]=0;
}
ans=max(ans,tol);
return;
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1,u,v;i<=m;i++)
{
scanf("%lld%lld",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
for (int i=1;i<=n;i++)
{
memset(vis,0,sizeof vis);
dfs(i,0,1);
}
printf("%lld",ans);
return 0;
}
11.16
总结:日常犯困,日常去厕所清醒,日常疯狂调试,不日常四个半小时的模拟赛。打了T1的60分暴力+特殊样例,T4的40分暴力+特殊样例,但是T1不知道为什么\(dfs\)爆栈了,所以没骗到特殊样例的分,T4特殊样例式子推错,也没骗到分,所以最后T1 30分,T4 20分,共50分,挂了50分。
关于T1:四个人,想了四个半小时,摸到了正解的边,但不多……
T1【细胞】
题目大意:
给定一棵以1为根、共\(n\)个节点的树,开始每个节点都有一个点权,每过一秒,每个点的权值会变成它父节点的权值。
现在给出\(m\)个操作,每次操作可以查询当前时间点\(v\)的权值,或给点\(v\)的权值加\(k\)(先转移再增加)。特别地,若\(v\)为叶子结点,那么每次它的权值会加上它父节点的权值。\((1\leqslant n\leqslant 5\times 10^{5})\)
解题思路:
直接模拟的复杂度为\(O(nm)\),不可以。我们可以想到,对于\(t\)时间询问的点\(v\),会直接对该点产生贡献的祖先点\(u\)满足\(dep_{u}=dep_{v}-t\)(若\(v\)是叶子节点,那么产生贡献的点满足\(dep_{u}\geqslant dep_{v}-t)\)。
所以对于每次查询\(v\)就是要求会对该点产生贡献的点的权值和。但由于存在修改操作,所以是带修查询,区间查询+单点修改,想到树状数组维护。(什么?我还是不会树状数组……)
离线存储\(m\)个操作,跑一遍\(dfs\),若节点\(u\)存在修改操作,那么就相当于是刚开始在\(dup_{u}-t\)处有一权值(此时路径上的查询操作已经处理完,此次修改不会影响路径前面的查询,只会修改路径后的查询——但要注意回溯以及偏移量),若存在查询操作,查询区间和就行。(也有可能是单点查询哦?)
代码
#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define mp make_pair
using namespace std;
const int N=5e5+5;//注意偏移量
int n,m;
vector <int> tr[N];
vector <pii> add[N];//时间,修改
vector <int> que[N];
int cnt[N<<1];
int ans[N];
queue <int> ot;
int dep[N];
int lowbit(int x) { return x&-x; }
void _update(int t,int x)
{
while (t<=(N<<1))
{
cnt[t]+=x;
t+=lowbit(t);
}
}
int _sum(int x)
{
int res=0;
while (x)
{
res+=cnt[x];
x-=lowbit(x);
}
return res;
}
void dfs(int x,int _fa)
{
bool flag=false;
if (tr[x].size()==1) flag=true;//叶子结点
dep[x]=dep[_fa]+1;
int _size1=add[x].size();
for (int i=0;i<_size1;i++) _update(dep[x]-add[x][i].first+N,add[x][i].second);//先单点修改
int _size2=que[x].size();
for (int i=0;i<_size2;i++)
{
int p=que[x][i];//查询时间点
if (flag) ans[p]+=_sum(dep[x]+N)-_sum(dep[x]-p+N-1);
else ans[p]+=_sum(dep[x]-p+N)-_sum(dep[x]-p+N-1);
}
int _size3=tr[x].size();
for (int i=0;i<_size3;i++)
{
int v=tr[x][i];
if (v==_fa) continue;
dfs(v,x);
}
for (int i=0;i<_size1;i++) _update(dep[x]-add[x][i].first+N,-add[x][i].second);//回溯
}
signed main()
{
cin>>n>>m;
for (int i=1,x,y;i<n;i++)//存储树
{
cin>>x>>y;
tr[x].push_back(y);
tr[y].push_back(x);
}
for (int i=1,x;i<=n;i++) cin>>x,add[i].push_back({0,x});//单点修改
for (int i=1;i<=m;i++)
{
char op;
int v,k;
cin>>op;
if (op=='+') cin>>v>>k,add[v].push_back({i,k});//单点修改
else cin>>v,que[v].push_back(i),ot.push(i);//区间查询
}
dfs(1,0);
while (!ot.empty()) cout<<ans[ot.front()]<<'\n',ot.pop();
return 0;
}
11.19
开头出奇地清醒,去了趟厕所反而越来越困。T1又是小清新构造题,后面几题的暴力分也很少,所以这次打的很不怎么样。
T1【基础构造练习题】
题目大意:
对于一棵树,记\(f(i)=\Sigma \ dis_{i,j}\),给出\(x\),要求构造存在\(f_{u}-f_{v}=x\)的一颗树,输出树节点数量最小值。\((0\leqslant x\leqslant 10^{18})\)
解题思路:
待补待补待补
待补代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
int T;
int n;
signed main()
{
scanf("%lld",&T);
while (T--)
{
scanf("%lld",&n);
if (!n) { printf("2\n"); continue; }
int x=floor(sqrt(n));
if (n<=x*x) printf("%lld\n",x*2+1);
else if (n<=(x+1)*x)
{
if (n%2==1) printf("%lld\n",x*2+3);
else printf("%lld\n",x*2+2);
}
else if (n<=(x+1)*(x+1)) printf("%lld\n",x*2+3);
}
return 0;
}
11.21
今天照常七点半左右到学校,结果入门发现氛围不对。打开手机,发现题目压缩包已经发了,我当时就是一个问号。
(一定是刚开始耽误的几分钟耽误我写T2了!!!)
然后就开始写题。这套题的难度对于我还好,不会出现打完暴力只能摆烂的情况。
(但出现了先躺尸式思考然后疯狂打暴力的情况)
T1第一眼看着花里胡哨,其实就是一个大除法;T3T4的暴力都很好打;T2思考了挺久,但是………………思路钻进死胡同里了,最后也只想出了部分分的做法。最后两个小时打了T4T3的暴力,本来我T3可以再优一些的,但是线段树写挂了调试了很久,最后时间不够了才转成更加暴力的写法。T1 100分,T2 20分,T3 50分,T4 75分,总分245,挂了-35分(oj上分数更少、排名更加靠后)
嘻嘻 按照评测机的大水数据的分算了话 我这次考了本机房第一!
T1【多项式】
题目大意:
分别给出两个多项式的系数\(H,G\),已知多项式\(H\)是\(G\)与\(F\)的乘积,求\(F\)的系数。(\(F,G\)的项数\(n\leqslant 2000\))
解题思路:
直接模拟大除法即可。
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=3010;
int m,g[N];
int k,h[N];
int f[N];
signed main()
{
scanf("%lld",&m);
for (int i=m;i>=1;i--) scanf("%lld",&g[i]);
scanf("%lld",&k);
for (int i=k;i>=1;i--) scanf("%lld",&h[i]);
for (int i=1;i+m-1<=k;i++)
{
f[i]=h[i]/g[1];
h[i]=0;
int x=f[i];
for (int j=1;j<=m;j++) h[i+j-1]=h[i+j-1]-x*g[j];
}
printf("%lld\n",k-m+1);
for (int i=k-m+1;i>=1;i--) printf("%lld ",f[i]);
return 0;
}
T2【撑杆跳】
题目大意:
给出\(n\)张卡片,对于每张卡片\(i\)有两个属性\(l_{i},c_{i}\),其中\(l_{i}\)表示可以向前、向后跳\(l_{i}\)的距离,\(c_{i}\)表示拥有这张卡片的代价。刚开始在位置\(0\),要求选择若干个卡片,使得可以要到任何位置并且花费最少,输出最小花费,若无法则输出\(-1\)。\((1\leqslant n\leqslant 300,1\leqslant l_{i}\leqslant 10^{9},1\leqslant c_{i}\leqslant 10^{5})\)
解题思路:
我连绿题都切不了,我太菜了www
观察样例可以发现,若要选择方案合法,对于所有被选卡片的最大公因数一定为1。(我不会证,好像和裴蜀定理有关。本人感性理解为:若选择的若干个数的最大公因数 \(g>1\),那么无论怎样跳跃一定只能跳到\(g\)的倍数的地方,无法跳到全部。)
再观察数据范围,容易想到\(dp\)。设\(dp\)方程式\(f_{i,j}\)表示从前\(i\)个数中选,选择数的最大公因数为\(j\)的最小花费。这样了话,\(dp\)转移方程式即为$$f_{i,gcd(j,k)}=min(f_{i,gcd(j,k)},f_{i-1,j}+c_{i})$$
于是乎兴奋地去看数据范围,看到\(l_{i}\)的值域后瞬间泄气
但是注意到\(l_{i}\)的值域,需要优化。
于是就开始瞪,感觉第一维或许可以不要。
继续瞪,越瞪越像\(dij\)中的\(dis_{v}=min(dis_{u},dis_{v}+w)\)(好像是叫三角形不等式?)
然后就再次想到万能的\(dijkstra\)算法(好吧我刚开始没想到)。将最大公因数设为点的编号,边权即为\(c_{v}\)。因为不好搞的值域,所以\(dis,vis\)数组都用\(map\)来搞。
复杂度不知道,反正能过(就很神奇)
然后就出现了
代码
#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define mp make_pair
using namespace std;
const int N=310;
const int inf=0x3f3f3f3f3f3f3f3f;
int T;
int n;
int v[N],c[N];
unordered_map <int,int> dis;
unordered_set <int> vis;
priority_queue < pii,vector <pii>,greater<pii> > q;
int gcd(int x,int y)
{
if (!y) return x;
return gcd(y,x%y);
}
void dijkstra()
{
dis.clear();
vis.clear();
while (!q.empty()) q.pop();
dis[0]=0;
q.push(mp(0,0));
while (!q.empty())
{
int u=q.top().second;
q.pop();
if (u==1) break;
if (vis.find(u)!=vis.end()) continue;
vis.insert(u);
for (int i=1;i<=n;i++)
{
int y=gcd(v[i],u);
if (dis.find(y)==dis.end()) dis[y]=inf;
if (dis[y]>dis[u]+c[i])
{
dis[y]=dis[u]+c[i];
q.push(mp(dis[y],y));
}
}
}
}
signed main()
{
scanf("%lld",&T);
while (T--)
{
scanf("%lld",&n);
for (int i=1;i<=n;i++) scanf("%lld",&v[i]);
for (int i=1;i<=n;i++) scanf("%lld",&c[i]);
dijkstra();
if (dis.find(1)!=dis.end()) printf("%lld\n",dis[1]);
else printf("-1\n");
}
return 0;
}
11.23
加密版:困困,困困困困困。困困困困困困困困困困困困困困困困困,困困困困困困困困困困困。困困困,困困困困困困困困困困,困困困困困困困困困困困困困困困困困困困困困,困困。困困困困,困困困困!
今天,模拟赛还没开始多久,就闻到了弥漫在空气中的糊味。于是,整个机房一起(?)冲到操场 看热闹 观察情况,不过十分钟后也差不多回去了。然后就开始打。T1题意叙述不清+简单,我都怀疑我是不是想假了;T2刚开始想成贪心了,但总感觉不对劲,及时换成了\(dp\);T3?我不道啊,推了下样例一不小心性质就出来了,打了特殊样例,但是我为什么不打暴力!!!为什么啊wwwwww;真是\(dij\)打多了,看到T4就想到\(dij\)的暴力写法了,然后又手推了下特殊样例,虽然时间耗得多了点,但分到手了。最后T1 100分,T2 70分,T3 12分,T4 30分,总分212,未挂分。
T1【公交车】
题目大意:
实在是不会翻译了……直接出吧
给出\(n\)个元素之间的\(m\)个关系,求节点数减去连通块数。\((1\leqslant n\leqslant 10^{5},1\leqslant m\leqslant 2\times 10^{5})\)
解题思路:
并查集直接做完了。我还搞什么带权并查集啊喂
代码太史就不放了
T2【自行车】
题目大意:
给出两个\(1\sim n\)的排列\(a,b\)。进行若干次操作,要求:若目前两个排列的第一个数不同,那么可以一次性删除掉两个排列的第一个数;若两个排列的第一个数相同,那么只能选择一个排列的第一个数删除。求清空两个排列的最小操作次数。\((1\leqslant n\leqslant 10^{6})\)
解题思路:
这道题我一定要多看多看再多看……
容易想到\(dp\)转移方程:
但这样的转移是\(O(n^{2})\),考虑优化。
显然,我并没有想出怎样优化
我们可以注意力惊人地注意到,满足\(a_{i}=b_{j}\)状态的只有\(n\)个,我们称它是特殊状态,称\(a_{i}\neq b_{j}\)是普通状态。
经过思考,我们会发现:对于普通状态\(f_{i,j}\),它一定是从\(f_{i-1,j-1}\),\(f_{i-2,j-2}\)…一直到特殊状态\(f_{i-t,j-t}\)转移而来的。也就是说,每个普通状态一定是通过某个特殊状态+某个固定值转移而来的。所以,只要处理出所有特殊状态的值,就可以得到答案了。
考虑设状态\(f_{k}\) 表示对于\(k=a_{i}=b_{j}\) 的特殊状态的转移答案,转移方程即$$f_{i}=min(getF_{i-1 , vis[a_{ i } ] } ,getF_{i,vis_[a_{i}]-1})+1$$这与最开始的状态转移方程一致,其中的\(getF\)转移时计算即可。
注意到在从特殊状态转移到普通状态时,\(i,j\)两维的差值始终不变,所以我们可以记\(g_{j-i}=\{i,f_{i}\}\)以记录\(i,j\)的差值。\(getF_{i,j}\)的转移方程即
神奇的代码
#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define mp make_pair
using namespace std;
const int N=1e6+5;
int n;
int a[N],b[N];
int vis[N];
int f[N];
pii g[N<<1];//记录相应差值的特殊状态
int get(int x,int y)
{
if (g[y-x+n].first==0) return max(x,y);//以防万一,搞个边界
return g[y-x+n].second+(x-g[y-x+n].first);
}
signed main()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++) scanf("%lld",&a[i]);
for (int i=1;i<=n;i++)
{
scanf("%lld",&b[i]);
vis[b[i]]=i;
}
for (int i=1;i<=n;i++)
{
f[i]=min(get(i-1,vis[a[i]]),get(i,vis[a[i]]-1))+1;//+n防溢出
g[vis[a[i]]-i+n]=mp(i,f[i]);
}
printf("%lld",get(n,n));
return 0;
}
11.26
昨天也打了模拟赛。但没补没总结。为什么呢。因为懒。
今天来了之后先犯困了一个坤小时。犯困的那两个半小时属于是连暴力都没法想怎么去写的那种。好不容易慢慢清醒了,又不想写了。随便打了个T3的暴力,又写了个T1的爆搜,结果爆搜炸了。
所以,今天昨天打的都很不怎么样。
结果考完之后,同学都说T3是去年提高组T3,我当时就是一个问号。
T2【插旗】
解题思路:
考虑深搜过程。对于一个节点的子树,它的最长链的长度一定会产生贡献(因为搜到最深后还到下一条链,这个长度很重要)。而搜到最后一条链时,它需要从最底端再回溯到子树之外,所以这条链的长度也会产生贡献。贪心地想,为了使得\(k\)最小,我们让最短链最后搜到。也就是说,对于一棵子树,它的最长链与最短链的长度会分别产生贡献。
具体咋写?细节有什么?我不道啊,我没写
T3【括号序列加强版】
题目大意:
解题思路:
暴力\(dp\)的复杂度是\(O(n^{3})\),而且分拿的很少 这对于我这个蒟蒻来讲非常不友好www 。于是考虑设一维状态\(f_{i}\)表示以\(s_{i}\)结尾的合法子串个数 可恶我本来也向这个方向想了但没想出来 。
对于\(f_{i}\),一定存在 \(j<i\) 使得 \(f_{i}\) 由 \(f_{j}\) 转移而来,使得 \([\ j+1,i\ ]\) 合法;而为了答案不重不漏,那么它一定是由最大的满足条件的 \(j\) 转移而来,也就是说\(f_{i}\) 一定是从以\(i\)结尾、长度最短的合法区间\([\ j+1,i\ ]\)转移而来。用 \(lst_{i}\) 记录 \(j+1\),有状态转移方程:
现在考虑怎么求\(lst_{i}\)。
因为\([\ lst_{i},i\ ]\)一定是最短的合法区间,那么它一定满足\(a_{lst_{i}}=a_{i}\)且区间\([\ lst_{i}+1,i-1\ ]\)合法。循环去求就行,若不存在,那么\(lst_{i}\)就为0。
代码如下↓
int j=i-1;
while (j>0&&a[i]!=a[j]) j=lst[j]-1;
lst[i]=j;
长得很像KMP求\(nxt\)数组的代码。
所以我们坚信它的时间复杂度和KMP的一样,均摊下来是\(O(n)\)
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n;
int a[N];
int lst[N];
int f[N];
int ans;
signed main()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++) scanf("%lld",&a[i]);
for (int i=1;i<=n;i++)
{
int j=i-1;
while (j>0&&a[i]!=a[j]) j=lst[j]-1;
if (a[i]==a[j])
{
lst[i]=j;
f[i]=f[lst[i]-1]+1;
ans+=f[i];
}
}
printf("%lld",ans);
return 0;
}