[NOIP]模拟17 题解
A.入阵曲
部分分很肥,正解写得常数稍大就会和暴力一个分,考试的时候写什么自己考虑。(滑稽
部分分的循环边界手抖写错了-25 (原本暴力分中的10分都没了啊啊啊)
没写挂的话应该有75,其实就是二维前缀和+暴力枚举点对统计+$a[i][j]$都相等时只枚举子矩形大小再乘上这种大小出现的次数。
正解:$(sum[r]-sum[l-1])\% K=0 \rightarrow sum[r]\% K=sum[l-1]\% K$
枚举行数$i,j$和列数$k$,维护i行和j行之间、k列左侧在%K意义下同余矩阵的个数,用桶来实现。
#include<cstdio> #include<iostream> #include<cstring> using namespace std; typedef long long ll; const int N=650; int n,m; ll K,ans; int a[N][N]; ll sum[N][N],sum1[N],cnt[1000005]; int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();} while(isdigit(ch))x=x*10+ch-'0',ch=getchar(); return x*f; } int main() { n=read();m=read();K=1LL*read(); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) a[i][j]=read(); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+1LL*a[i][j]; for(int i=0;i<n;i++) for(int j=i+1;j<=n;j++) { cnt[0]=1; for(int k=1;k<=m;k++) { sum1[k]=(sum[j][k]-sum[i][k]+K)%K; ans+=cnt[sum1[k]]; cnt[sum1[k]]++; } for(int k=1;k<=m;k++)cnt[sum1[k]]=0; } cout<<ans<<endl; return 0; }
B.将军令
我考场都能想到的sb贪心。每次取出深度最大且未被覆盖的点,在它的K级祖先上驻扎即可。前者用堆维护,后者直接暴力修改覆盖状态,注意向上修改时不要只遍历和它在一条链上的。
正确性?读者自证不难。(逃
#include<cstdio> #include<iostream> #include<cstring> #include<queue> #define pa pair<int,int> #define re register using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();} while(isdigit(ch))x=x*10+ch-'0',ch=getchar(); return x*f; } const int N=100005; int n,K,t; int to[N<<1],nxt[N<<1],head[N],tot; inline void add(int x,int y) { to[++tot]=y; nxt[tot]=head[x]; head[x]=tot; } int dep[N],fa[N]; priority_queue<pa> q; void dfs1(int x,int deep) { dep[x]=deep; for(re int i=head[x];i;i=nxt[i]) { int y=to[i]; if(!dep[y])dfs1(y,deep+1),fa[y]=x; } return ; } int cover[N],anc,ans; void getan(int x,int rest) { if(!rest||x==1) { anc=x; return ; } getan(fa[x],rest-1); return ; } void dfs2(int x,int rest,int f) { cover[x]=1; if(!rest)return ; for(re int i=head[x];i;i=nxt[i]) { int y=to[i]; if(y==f)continue; dfs2(y,rest-1,x); } return ; } int main() { n=read();K=read();t=read(); for(re int i=1;i<n;i++) { int x=read(),y=read(); add(x,y);add(y,x); } dfs1(1,1); for(re int i=1;i<=n;i++) q.push(make_pair(dep[i],i)); while(!q.empty()) { int x=q.top().second; q.pop(); if(cover[x])continue; getan(x,K); //cout<<anc<<endl; dfs2(anc,K,-1); ans++; } cout<<ans<<endl; return 0; }
C.星空
区间状态反转可以看作区间异或1,但区间操作不好处理,考虑通过差分转化为单点操作。即把对原数组上$[L,R]$区间的操作转化为差分数组上$L-1$和$R$两个点的操作。这里用到了异或差分:$dif[i]=a[i]\ xor\ a[i+1]$
所以问题变成了:有一个01串,每次对其中两个点$xor\ 1$,需要多少次把这个串的每一位都变成0。(差分数组全0对应原数组全1)
可以发现,消去两个1的费用与他们之间的距离有关。bfs预处理出来后,问题再次得到转化:每次取出一对物品,每对物品取出都有一定代价,如何取出使得代价最小。由于不亮的灯泡很少,这个问题完全可以状压解决。
#include<cstdio> #include<iostream> #include<cstring> #include<queue> #include<vector> using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();} while(isdigit(ch))x=x*10+ch-'0',ch=getchar(); return x*f; } const int N=40005; int n,m,K; int a[N],op[N],dif[N]; int pos[N],sz; int vis[N],dis[20][20],d[N]; int find0[N<<1],dp[N<<1]; void bfs(int node) { memset(d,0,sizeof(d)); memset(vis,0,sizeof(vis)); queue<int> q; q.push(node); vis[node]=1;d[node]=0; while(!q.empty()) { int x=q.front(); q.pop(); for(int i=1;i<=m;i++) { int s1=x+op[i],s2=x-op[i]; if(s1<=n&&!vis[s1]) { q.push(s1); d[s1]=d[x]+1; vis[s1]=1; } if(s2>=0&&!vis[s2]) { q.push(s2); d[s2]=d[x]+1; vis[s2]=1; } } } } int main() { n=read();K=read();m=read(); for(int i=1;i<=K;i++) a[read()]=1; for(int i=1;i<=m;i++) op[i]=read(); for(int i=0;i<=n;i++) { dif[i]=a[i]^a[i+1]; if(dif[i])pos[++sz]=i; } for(int i=1;i<=sz;i++) { bfs(pos[i]); for(int j=1;j<=sz;j++) dis[i][j]=d[pos[j]]; } for(int s=0;s<(1<<sz);s++) { find0[s]=sz-1; for(int i=0;i<sz;i++) if(((s>>i)&1)==0) { find0[s]=i; break; } } memset(dp,0x3f,sizeof(dp)); dp[0]=0; for(int s=0;s<(1<<sz);s++) for(int i=find0[s]+1;i<sz;i++) { if(((s>>i)&1)==0&&dis[find0[s]+1][i+1]) { int now=(s|(1<<find0[s])|(1<<i)); dp[now]=min(dp[now],dp[s]+dis[find0[s]+1][i+1]); //cout<<dp[now]<<endl; } } cout<<dp[(1<<sz)-1]<<endl; return 0; }
兴许青竹早凋,碧梧已僵,人事本难防。