您的位置:首页 > 其它

NOIP模拟53

2021-09-15 08:18 176 查看

我在时光斑驳深处,聆听到花开的声音。

前言

这套题好像是随便拼接起来的,来自三套不同的题,最后一道还是学长出的(nb

场上为数不多的几次死磕一道题正解,大概有三个小时吧(惭愧,前两个小时看错题了,一直以为是外向树,幸亏后面发现部分结论仍然适用。。

然后码完之后错误不算太多就是没写对拍(然后我就 30pts 了。。。

考完之后写了一个对拍整了几组数据就过了(也许是题库数据水吧,WindZR已经把我给 Hack 了。。。但是懒地改了。

果然打出来没有对拍的正解就是个废物。。

T1 ZYB和售货机

解题思路

看到这个题的第一眼就发现是一个 n 个点 n 条边的图,一开始是以为 f_i 互不相同只需要找到环就好了,然后我的考试时间就少了一个小时。

后来又看了一边题,以为是个外向树,大概和 Tarjan 有关系,然后我的考试时间就又少了一个小时。

这个题显然是一个由多个内向树构成的森林,我们只需要对于每一棵树进行处理就好了。

不难发现其实在所有物品都有剩余也就是不为零的时候我们是可以随便购买的,因此将所有可购买的点都用最大的利润也就是最小如价买到只剩下一个。

然后对于每一棵树,限制他的只是环里的点,分情况讨论,这个点的最小入价是否来自于环里,但凡有一个不是我们就可以直接断开这个点的连接,这样就可以把所有的点都以最优的方式购买了。

对于在环内并且最小入价的点也在环内的点我们先按不在环内的最小值计入答案最后再把所有受限制的点选择删去贡献最小的就好了。

我的代码实现就比较恶心了(也就 170 行,所以下面也会放一下 fengwu 的 30 行超短代码。。

code

170行的。。。

#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define f() cout<<"Failed"<<endl
using namespace std;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=1e5+10,INF=1e18;
struct Node
{
int id,to,c,d,a;
vector<int> fro;
}s
;
int n,ans,cnt,top,id
,sta
;
bool flag,vis
;
pair<int,int> jl;
vector<int> v
,vec;
queue<int> q;
void dfs(int x)
{
if(flag) return;
sta[++top]=x;id[x]=top;
for(int i=0;i<s[x].fro.size();i++)
{
int to=s[x].fro[i];
if(id[to])
{
flag=true;
jl.first=id[to];
jl.second=id[x];
return ;
}
dfs(to);
}
top--;
}
signed main()
{
freopen("goods.in","r",stdin); freopen("goods.out","w",stdout);
n=read();
for(int i=1;i<=n;i++)
{
s[i].to=read(); s[i].c=read();
s[i].d=read(); s[i].a=read();
s[i].id=i;
}
for(int i=1;i<=n;i++) s[s[i].to].fro.push_back(i);
for(int i=1;i<=n;i++)
{
int minn=INF;
for(int j=0;j<s[i].fro.size();j++)
minn=min(minn,s[s[i].fro[j]].c);
if(minn>s[i].d) continue;
ans+=(s[i].d-minn)*(s[i].a-1); s[i].a=1;
}
for(int i=1;i<=n;i++)
{
if(vis[i]||!s[i].fro.size()) continue;
q.push(i);cnt++;
while(!q.empty())
{
int x=q.front();q.pop();
if(vis[x]) continue;
vis[x]=true;v[cnt].push_back(x);
for(int j=0;j<s[x].fro.size();j++)
q.push(s[x].fro[j]);
}
}
memset(vis,false,sizeof(vis));
for(int i=1;i<=cnt;i++)
{		top=0; flag=false;
int pos=0;
while(pos<v[i].size())
{
if(!s[v[i][pos]].fro.size()) pos++;
else break;
}
dfs(v[i][pos]);
if(jl.first==jl.second)
{
for(int j=0;j<v[i].size();j++)
{
int minn=INF,x=v[i][j];
for(int k=0;k<s[x].fro.size();k++)
minn=min(minn,s[s[x].fro[k]].c);
if(minn>s[x].d) continue;
ans+=s[x].d-minn; s[x].a=0;
}
continue;
}
vector<int>().swap(vec);
bool jud=false;
for(int j=jl.first;j<=jl.second;j++) vis[sta[j]]=true;
for(int j=jl.first;j<=jl.second;j++)
{
int minn=INF,pos=0,x=sta[j];
for(int k=0;k<s[x].fro.size();k++)
if(minn>s[s[x].fro[k]].c) minn=s[s[x].fro[k]].c,pos=s[x].fro[k];
if(minn>s[x].d)
{
if(vis[pos]) break;
continue;
}
if(vis[pos]){jud=true;break;}
}
if(!jud)
{
for(int j=0;j<v[i].size();j++)
{
int minn=INF,x=v[i][j];
for(int k=0;k<s[x].fro.size();k++)
minn=min(minn,s[s[x].fro[k]].c);
if(minn>=s[x].d) continue;
ans+=s[x].d-minn; s[x].a=0;
}
continue;
}
for(int j=jl.first;j<=jl.second;j++)
{
int minn=INF,sec=INF,pos2=0,pos=0,x=sta[j];
for(int k=0;k<s[x].fro.size();k++)
if(minn>=s[s[x].fro[k]].c)
{
if(minn==s[s[x].fro[k]].c&&vis[s[s[x].fro[k]].c]) pos=s[s[x].fro[k]].c;
minn=s[s[x].fro[k]].c;
pos=s[x].fro[k];
}
for(int k=0;k<s[x].fro.size();k++)
if(pos!=s[x].fro[k]&&vis[s[x].fro[k]])
{
vec.push_back(-INF);
goto V;
}
if(minn>=s[x].d){if(vis[pos])vec.push_back(-INF);continue;}
if(!vis[pos]){ans+=s[x].d-minn;continue;}
for(int k=0;k<s[x].fro.size();k++)
if(sec>s[s[x].fro[k]].c&&s[x].fro[k]!=pos)
sec=s[s[x].fro[k]].c,pos2=s[x].fro[k];
if(sec==INF||sec>=s[x].d)
{
vec.push_back(s[x].d-minn);
continue;
}
vec.push_back(sec-minn);
ans+=s[x].d-sec; s[x].a=0;
V:;
}
sort(vec.begin(),vec.end());
for(int j=1;j<vec.size();j++)
if(vec[j]>0)
ans+=vec[j];
for(int j=0;j<v[i].size();j++)
{
int minn=INF,x=v[i][j];
for(int k=0;k<s[x].fro.size();k++)
if(!vis[s[x].fro[k]])
minn=min(minn,s[s[x].fro[k]].c);
if(minn>=s[x].d) continue;
ans+=(s[x].d-minn)*s[x].a; s[x].a=0;
}
}
printf("%lld",ans);
return 0;
}

30行的极致体验

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define re register int
const int N=1e6+5;
const int inf=1e9;
int n,f
,c
,d
,a
,v
;
int mx
,nx
,mn,bel
,cnt;
ll ans;
void dfs(int x){
if(bel[x]==cnt){ans-=mn;return ;}
if(bel[x])return ;
bel[x]=cnt;
if(!mx[x])return ;
ans+=1ll*v[mx[x]]*a[x];
mn=min(mn,v[mx[x]]-v[nx[x]]);
if(mx[x]!=x)dfs(mx[x]);
}
signed main(){
freopen("goods.in","r",stdin);freopen("goods.out","w",stdout);
scanf("%d",&n);
for(re i=1;i<=n;i++)scanf("%d%d%d%d",&f[i],&c[i],&d[i],&a[i]);
for(re i=1;i<=n;i++){
v[i]=d[f[i]]-c[i];
if(v[i]>=v[mx[f[i]]])nx[f[i]]=mx[f[i]],mx[f[i]]=i;
else if(v[i]>=v[nx[f[i]]])nx[f[i]]=i;
}
for(re i=1;i<=n;i++)if(!bel[i])mn=inf,++cnt,dfs(i);
printf("%lld",ans);
}

T2 ZYB玩字符串

解题思路

官方题解写的特别明白就是两个 DP 柿子。

主要解释一下暴力为什么不对,在我们暴力扫的时候如果同时有两个可以消去的一个可能是对于正确答案不可以消去的,也就是他是由一段后缀加上一段前缀的组成方式,由于某些玄学因素看似合法了。。

给出对于这份代码的两组Hack数据(如果这个做法是正确的话他正着扫和反着扫得分是一样的,但是在 OJ 上却是 30 和 50)

ddaddadad
输出应该是
dad
但是反着扫的结果是
ddaddadad

cdcdcdccc
输出应该是
cdc
但是正着扫的结果是
cdcdcdccc

下面也会给出一组造随机数据的程序,其实就是随一个字符串然后向里面随机插入就好了。

正解 DP 方程的正确性的前提是要保证所扫到的字串的长度要是整个串长度的因数,f_{i,j} 表示区间 [i,j] 能否合法或者能否消光。

转移方程就是下面的两个(尽管有关于代码的不能用 Latex,但是代码快真的好丑)

$$f_{i,j}|=f_{i,j-k\times len}\&f_{j-k\times len+1,j},k\times len\in[1,j-i]$$ 注意这里的 p 就是我们扫到的字符串下标从 1 开始,显然转移应该从小的区间到大的区间,因此区间 DP 就可以了。 最后有几个小优化:s 的每种字符是不是 p 中对应的整数倍,p 的开头和结尾一定是 S 的开头或者结尾,记录一下 p 是否已经被计算过。 然后我们的时间就 $3000ms\rightarrow500ms\rightarrow50ms$ ## code ### 正解 ```cpp #include<bits/stdc++.h> #define int long long #define ull unsigned long long #define f() cout<<"Failed"<<endl using namespace std; inline int read() { int x=0,f=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*f; } const int N=300; int T,n,all[26],tot[26],f

; char s
,ch
; unordered_map<string,bool> mp; void solve() { scanf("%s",s+1); n=strlen(s+1); memset(all,0,sizeof(all)); mp.clear(); for(int i=1;i<=n;i++) all[s[i]-'a']++; for(int len=1;len<=n;len++) if(n%len==0) for(int l=1;l+len-1<=n;l++) { int r=l+len-1; memset(tot,0,sizeof(tot)); if(s[l]!=s[1]||s[r]!=s
) continue; string c; for(int i=l;i<=r;i++) ch[i-l+1]=s[i],c.push_back(s[i]); if(mp.find(c)!=mp.end()) continue; mp.insert(make_pair(c,true)); for(int i=1;i<=len;i++) tot[ch[i]-'a']++; for(int i=0;i<26;i++) if(tot[i])if(all[i]%tot[i]) goto V; memset(f,0,sizeof(f)); for(int i=1;i<=n;i++) f[i][i]=(s[i]==ch[1]); for(int ll=2;ll<=n;ll++) for(int i=1;i+ll-1<=n;i++) { int j=i+ll-1; f[i][j]|=f[i][j-1]&(s[j]==ch[(j-i)%len+1]); for(int k=1;j-k*len>=i&&!f[i][j];k++) f[i][j]|=f[i][j-k*len]&f[j-k*len+1][j]; } if(f[1]
) { for(int i=1;i<=len;i++) printf("%c",ch[i]); printf("\n"); return ; }V:; } } signed main() { freopen("string.in","r",stdin); freopen("string.out","w",stdout); T=read(); while(T--) solve(); return 0; } ``` ### 数据生成器 ```cpp #include <bits/stdc++.h> using namespace std; int main() { srand((int)time(0)); int T = 10, sum = 0;cout<<T<<endl; while (T--) { int m =3; string p = ""; int k = rand() % 26 + 1; while (m--) p = p + (char)('a' + rand() % k); int num = 3; string s = ""; while (num--) { int k = rand() % (s.length() + 1); s = s.substr(0, k) + p + s.substr(k); } cout << s << endl; //fprintf(stderr, "%d\n", s.length()); sum += s.length(); } //fprintf(stderr, "%d\n", sum); } ``` # T3 午餐 ## 解题思路 很玄学的一道最短路题。 第一边是通过无法学会的人为起点通过 **不用进行的聚餐** 进行转移 $h_i$ 表示只考虑 -1 的情况下最早的学会时间。 对于一条边也就是一次聚餐时间为 $L,R$,我们要从 x 到 to 转移方程就是: $$h_{to}=L+1,h_x>R且h_{to}<L+1$$ 再跑一次最短路从 1 开始,求出最终的答案然后这次跑的就是 **用进行的聚餐** 设 $lim=\max\{L,h_{to},ans_x\}$,则 $ans_{to}=lim,lim\le R且 ans_{to}>lim$ 最后输出的时候判断是否相矛盾就好了,注意如果一个点最终不能学会那么他一定不可能和 1 连边。 如果一条边连接的有最终不能学会的,那么时间最好就是 $L$ 如果这场聚餐无所谓其实可以是 $[L,R]$ 中的任何一个数,但是由于题库的 SPJ 有点小问题所以只可以是 L 。 如果有必要举行那么答案就是 $\max\{L,ans_x,ans_y\}$ ## code ```cpp #include<bits/stdc++.h> #define int long long #define ull unsigned long long #define f() cout<<"Failed"<<endl using namespace std; inline int read() { int x=0,f=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*f; } const int N=2e6+10,INF=0x3f3f3f3f3f3f3f3f; int n,m,s
,ans
,h
; int tot=1,head
,nxt[N<<1],ver[N<<1],val[N<<1],var[N<<1]; bool vis
; struct Queue{int l,r,num
;void push(int x){num[++r]=x;}void pop(){l++;}int front(){return num[l];}bool empty(){return l>r;}}q; void add_edge(int x,int y,int l,int r){ver[++tot]=y;nxt[tot]=head[x];val[tot]=l;var[tot]=r;head[x]=tot;} signed main() { freopen("lunch.in","r",stdin); freopen("lunch.out","w",stdout); srand(time(0)); n=read(); m=read(); memset(ans,0x3f,sizeof(ans)); for(int i=1,x,y,l,r;i<=m;i++) x=read(),y=read(),l=read(),r=read(),add_edge(x,y,l,r),add_edge(y,x,l,r); for(int i=1;i<=n;i++) s[i]=read(); for(int i=1;i<=n;i++) if(s[i]==-1) h[i]=INF,q.push(i),vis[i]=true; while(!q.empty()) { int x=q.front();q.pop();vis[x]=false; for(int i=head[x];i;i=nxt[i]) { int to=ver[i],l=val[i],r=var[i]; if(h[x]<=r||h[to]>=l+1) continue; h[to]=l+1; if(!vis[to]) q.push(to),vis[to]=true; } } q.push(1);ans[1]=0;vis[1]=true; while(!q.empty()) { int x=q.front();q.pop();vis[x]=false; for(int i=head[x];i;i=nxt[i]) { int to=ver[i],l=val[i],r=var[i]; int lim=max(l,max(h[to],ans[x])); if(lim>r||ans[to]<=lim) continue; ans[to]=lim; if(!vis[to]) q.push(to),vis[to]=true; } } for(int i=1;i<=n;i++)if(s[i]==1&&ans[i]>=INF){printf("Impossible");return 0;} for(int i=head[1];i;i=nxt[i])if(s[ver[i]]==-1){printf("Impossible");return 0;} for(int i=1;i<=m;i++) { int l=val[i<<1],r=var[i<<1],x=ver[i<<1],y=ver[i<<1|1]; if(ans[x]==-1||ans[y]==-1){printf("%lld\n",l);continue;} printf("%lld\n",max(ans[x],ans[y])>r?l:max(l,max(ans[x],ans[y]))); } return 0; } ``` # T4 计数 ## 解题思路 rvalue 学长不仅题目出的好而且官方题解写的也非常 nb,于是我们直接。。。(这个题的转移是有类似于区间性质的因此我选择了记忆化搜索的写法) 首先我们可以根据前序遍历的定义分析出一棵符合前序遍历约束的树的一些性质: 1. 这棵树的编号满足小根堆性质 2. 这棵树的某个子树编号连续,为 $[root,root+size-1]$ 3. 这棵树左子树中的结点编号都小于右子树 4. 这棵树任意一个有左子节点的节点的左子节点编号必定为该节点编号 +1 然后我们来看中序遍历约束. 首先我们强制 $a<b$ , 然后将限制转化为“强制 a 在 b 前”或“强制 a 在 b 后”. 因为$a<b$, 再加上上面的性质, 所以 a 和 b 只有两种位置关系: 1. a 是 b 的祖先 2. a 在 LCA 的左子树且 b 在LCA 的右子树. 我们来分析一下两种限制条件: 1. 限制1: 强制 a 在 b 后: 这种情况下 a 一定是 b 的祖先, 因为如果他们的位置关系是第二种的话,a 在中序遍历中一定在 b 之前. 所以 b 一定在 a 的左子树中 2. 限制2: 强制 a 在 b 前: 这种情况下两种位置关系都有可能, 如果是位置关系 1 则 b 在 a 的右子树, 如果是位置关系 2 则 b 不在该子树中. 这种限制和 b 不在 a 的左子树中等价 因为子树中结点编号连续, 所以上述限制条件都可以转化为对 的左子树大小的限制. 对于第一种限制, 则根据性质 2 和 4, 左子树大小不小于 $b-a$ 是该限制被满足的充要条件. 对于第二种限制, 左子树大小小于 $b-a$ 是该限制被满足的充要条件. 所以我们只需要将每个节点的左子树大小限制预处理出来即可. 方式就是对于每个 a 都计算出限制 1的 b 的最大值和限制2 的 b 的最小值. 定义状态 $f_{i,j}$ 表示根节点为 i 子树大小为 j 的合法方案数量, 枚举左子树分配到的子树大小转移即可. 也即 $$\displaystyle f_{i,j}=\sum_{k=low_i}^{hig_i}f_{i+1,k}\times f_{i+k+1,j-k-1}$$ k 就是可行的左子树大小 ## code ```cpp #include<bits/stdc++.h> #define int long long #define ull unsigned long long #define f() cout<<"Failed"<<endl using namespace std; inline int read() { int x=0,f=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*f; } const int N=3e3+10,M=3e3+10,mod=1e9+7,INF=1e18; int n,m,T,f
[M],low
,hig
; struct Node { int x,y; }s[M]; int dfs(int x,int siz) { if(~f[x][siz]) return f[x][siz]; if(siz==0||x==n) return f[x][siz]=1; int rec=0,fro=low[x],to=min(siz-1,hig[x]); for(int i=fro;i<=to;i++) rec=(rec+dfs(x+1,i)*dfs(x+i+1,siz-i-1))%mod; return f[x][siz]=rec; } void solve() { n=read(); m=read(); memset(f,-1,sizeof(f)); for(int i=1;i<=n;i++) low[i]=0,hig[i]=n-i+1; for(int i=1;i<=m;i++) s[i].x=read(),s[i].y=read(); for(int i=1;i<=m;i++) if(s[i].x<s[i].y) hig[s[i].x]=min(hig[s[i].x],s[i].y-s[i].x-1); else low[s[i].y]=max(low[s[i].y],s[i].x-s[i].y); printf("%lld\n",dfs(1,n)); } signed main() { freopen("count.in","r",stdin); freopen("count.out","w",stdout); T=read(); while(T--) solve(); return 0; } ```
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: