格雷码是一种特殊的 \(n\) 位二进制串排列法,要求相邻的两个二进制串刚好有一位不一样,环状相邻。node
生成方法:数组
求 \(n\) 位格雷码的第 \(k\) 个串。函数
\(1\leq n\leq 64,0\leq k\leq 2^n\) .优化
考虑一个跟康托展开很是类似的思路。spa
首先看第一位,若是是 1 那么说明它前面已经能够肯定至少排了 \(2^n\) 个 0 开头的二进制串。code
那么这样就能够肯定第一位是 0 仍是 1 ,看 \(k\) 的大小就行了。递归
后面的也是同理,每次判断完以后:队列
复杂度是 \(\mathcal{O}(n)\) 的。(不过这种题也不须要考虑这个吧)字符串
最后:通过 CSP-S2020 ,我发现 \(k<2^n\leq 2^{64}\) (get
写代码的时候注意溢出问题,要开 unsigned long long
特别是左移的地方要注意。
//Author: RingweEH #define ull unsigned long long const int N=70; int n,a[N]; ull k; int main() { n=read(); scanf( "%llu",&k ); ull now=1ull<<(n-1); for ( int i=n-1; i>=0; i-- ) { if ( (k>>i)&1 ) a[i]=1,k=(now<<1)-k-1; else a[i]=0; } for ( int i=n-1; i>=0; i-- ) printf( "%d",a[i] ); return 0; }
给定一棵以 \(1\) 为根的括号树,每一个点恰有一个 (
或 )
,定义 \(s(i)\) 为将根节点到 \(i\) 号点的简单路径按通过顺序排列造成的字符串。
设 \(k(i)\) 表示 \(s(i)\) 中互不相同的子串是合法括号串的个数。求 \(\forall1\leq i\leq n,\sum i\times k(i)\) ,这里的求和表示异或和。
\(n\leq 5e5\)
终于补完模拟赛来继续写题了
题外话:今天模拟赛也有一道括号匹配题,可是是奇妙的贪心,要写两个栈+一个双端队列(
STL永远的神!
显然若是对这棵树进行 DFS ,那么根到 \(i\) 的路径上的点能够用栈获得。
那么一遍 DFS 就能够处理出根到 \(i\) 的路径上 互不相同的子串是合法括号串 的个数。
设 \(endpos[i]\) 为以节点 \(i\) 结尾的,根到 \(i\) 中互不相同的合法括号子串的个数。相似括号匹配的思路,若是当前为左括号那么直接进栈;若是是右括号且栈不为空,那么栈顶就能和当前点配对,这样就造成了一个新的合法子串 sta.top(),i
,那么当前节点的 \(endpos\) 就能够由这一对括号以前的东西推知。
令 \(fa[i]\) 表示括号树上点 \(i\) 的父亲节点,那么 \(endpos[i]=endpos[fa[sta.top()]]+1\) (由于 \(endpos[fa[sta.top()]]\) 这些串都能和当前这一对括号接起来,成为一个新的合法子串;或者当前这个单独成串)
最后 \(k(i)\) 就是根到 \(i\) 的路径上全部的 \(endpos\) 之和,这个也能够在 DFS 的时候顺带求出来。
注意递归完以后要记得还原,pop 掉的左括号搞回去,push 的左括号拿出来。
//Author: RingweEH const int N=5e5+10; int n,fa[N],endpos[N]; ll f[N]; stack<int> sta; vector<int> son[N]; char s[N]; void dfs( int u ) { bool pu=0; int las=0; if ( s[u]=='(' ) sta.push( u ),pu=1; else if ( !sta.empty() ) { endpos[u]=endpos[fa[sta.top()]]+1; las=sta.top(); sta.pop(); } f[u]=f[fa[u]]+endpos[u]; for ( int i=0; i<son[u].size(); i++ ) dfs( son[u][i] ); if ( pu && sta.top()==u ) sta.pop(); if ( las ) sta.push( las ); } int main() { n=read(); scanf( "%s",s+1 ); for ( int i=2; i<=n; i++ ) fa[i]=read(),son[fa[i]].push_back(i); endpos[1]=0; f[1]=0; dfs( 1 ); ll ans=0; for ( int i=1; i<=n; i++ ) ans^=(i*f[i]); printf( "%lld\n",ans ); return 0; }
给定一棵大小为 \(n\) 的数,初始时每一个节点上都有一个 \(1\sim n\) 的数字,且每一个 \(1\sim n\) 的数字都只 刚好 在一个节点上出现。
进行 刚好 \(n-1\) 次删边操做,每次操做须要选一条 未被删去的边 ,交换两个端点的数字,并删边。
删完后,按数字 \(1\sim n\) 的顺序将所在节点编号依次排列获得排列 \(P_i\) ,求能获得的字典序最小的 \(P_i\) .
\(n\leq 2000\)
这种 “字典序最小”的题一看就很像贪心。
题外话:今天模拟赛也有 “字典序最小”的贪心题,但是我一连胡错了两次,最后思路对了还调了半天(
要想贪心,确定是让某个小编号尽量地在最后的排列中获得小的权值。那么考虑如何让一个数字最终达到某个特定的位置。
假设如今有一条路径 :\(start\to a\to b\to c\to d\to end\) ,并将这些边从左到右依次标号为 \(1,2,3,4,5\) 。
那么,假设咱们如今想要把 \(start\) 节点上的数字转移到 \(end\) 节点上。能够发现:
因为要前面的数尽量小,那么枚举填数的时候必定是从小到大的。每次利用以上的性质判断是否可以填到这个位置,直到找到一个能填且最小的位置,并加上这个点给后面的限制。
看到图论里面的限制其实一个很天然的想法就是:连边为限制 ,可是这里的限制是针对边的,那么就能够考虑把边转化成点。
对原树上的每个点建一张图,图上每一个点表明连的一条边,并记录这个点钦定的第一条边和最后一条边。这张图上的一条有向边表示出点在入点以后立刻选择。点与点之间建的图是独立的。
考虑什么状况下会出现矛盾。
图不能被分割成独立的若干条链(这样就会有一条边后面要连多条边,或是出现环,显然不合法)
钦定的第一个点和最后一个点有入/出边(显然不合法)
第一个点和最后一个点在同一条链上,可是还有点不在这条链中。(已经造成了完备惟一的删边方案,可是有边还没删)
这些条件的矛盾分别表现为:
而后各类判断就行了。题目很 毒瘤 细节(也有多是我写烦了),实现时要注意。
代码中有详细的注释,若是发现本身挂了能够看看注释,找找有没有漏掉的条件。
我发现我最近善于把题目写得码农(
//Author: RingweEH const int N=2010; int n,pos[N],head[N],tot; //pos:数字 i 初始节点位置 struct edge { int to,nxt; }e[N<<1]; struct node_graph //每一个点所创建的图 { int fir,las,num,fa[N]; //钦定的第一条边,最后一条,边(点)数,并查集 bool ine[N],oue[N]; //是否有入/出边 void clear() { fir=las=num=0; for ( int i=1; i<=n; i++ ) fa[i]=i,ine[i]=oue[i]=0; } int find( int x ) { return x==fa[x] ? x : fa[x]=find(fa[x]); } }g[N]; void add( int u,int v ) { e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; g[u].num++; e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; g[v].num++; } int dfs1( int u,int fro_edge ) { int res=n+1; if ( fro_edge && (!g[u].las || g[u].las==fro_edge ) ) //尚未终点或者终点就是这条边 { if ( !g[u].oue[fro_edge] && !(g[u].fir && g[u].num>1 && g[u].find(fro_edge)==g[u].find(g[u].fir)) ) //这条边(点)在这个点的图里面尚未出边,并且不能: //有起点,总点数大于1,且已经在一条链里面了 res=u; } for ( int i=head[u]; i; i=e[i].nxt ) { int v=e[i].to,to_edge=i/2; if ( fro_edge==to_edge ) continue; //防止沿着双向边搜回去,tot是从1开始的,因此同一条双向边/2向下取整是同样的 if ( !fro_edge ) //前面没有链的状况 { if ( !g[u].fir || g[u].fir==to_edge ) //没有钦定起点,或者起点就是当前边 { if ( g[u].ine[to_edge] ) continue; //若是有入边了就不能当起点 if ( g[u].las && g[u].num>1 && g[u].find(to_edge)==g[u].find(g[u].las) ) continue; //起点和终点已经在一条链里面了 res=min( res,dfs1( v,to_edge ) ); } else continue; } else //前面有链,日后接的状况 { if ( fro_edge==g[u].las || to_edge==g[u].fir || g[u].find(fro_edge)==g[u].find(to_edge) ) continue; //若是上一条链的尾点是终点,那么后面不能接链;若是这条边是起点,那么不能被接; //若是已经在一条链上了,也不能被接 if ( g[u].oue[fro_edge] || g[u].ine[to_edge] ) continue; //已经接过了 if ( g[u].fir && g[u].las && g[u].num>2 && g[u].find(fro_edge)==g[u].find(g[u].fir) && g[u].find(to_edge)==g[u].find(g[u].las) ) continue; //从起点来的链,接上去终点的链,且还有点不在链上 res=min( res,dfs1( v,to_edge ) ); } } return res; } int dfs2( int u,int fro_edge,int endpos ) { if ( u==endpos ) { g[u].las=fro_edge; return 1; } //到终点了,使命完成 for ( int i=head[u]; i; i=e[i].nxt ) { int v=e[i].to,to_edge=i/2; if ( fro_edge!=to_edge ) { if ( dfs2( v,to_edge,endpos ) ) //后面可行 { if ( !fro_edge ) g[u].fir=to_edge; //前面没有了,这个就是起点 else { //更新有无出入边的限制,并查集合并 g[u].oue[fro_edge]=g[u].ine[to_edge]=1; g[u].num--; g[u].fa[g[u].find(fro_edge)]=g[u].find(to_edge); } return 1; } } } return 0; } int main() { int T=read(); while ( T-- ) { tot=1; memset( head,0,sizeof(head) ); n=read(); for ( int i=1; i<=n; i++ ) g[i].clear(),pos[i]=read(); for ( int i=1,u,v; i<n; i++ ) u=read(),v=read(),add( u,v ); if ( n==1 ) { printf( "1\n" ); continue; } int p; for ( int i=1; i<=n; i++ ) { p=dfs1( pos[i],0 ); dfs2( pos[i],0,p ); //1用来搜方案,2用来加限制 printf( "%d ",p ); } printf( "\n" ); } return 0; }
有 \(1\sim n\) 种烹饪方法和 \(1\sim m\) 种食材,使用 \(i\) 方法,食材为 \(j\) 的一共有 \(a_{i,j}\) 道菜。
对于一种包含 \(k\) 道菜的方案而言:
求有多少种不一样的搭配方案,对 \(998244353\) 取模。\(1\leq n\leq 100,1\leq m\leq 2000\)
对于方阵 \(a\) ,题目要求就至关因而:
考虑容斥,那么就是:每行至多取一个的方案 - 取了 0/1 个的方案 - 存在一列取了超过半数的方案(显然这样的列至多有一个)
对于每行至多取一个的总方案,来一遍 DP ,令 \(g[i][j]\) 表示到第 \(i\) 行,取了 \(j\) 个的方案数,\(sum[i]=\sum a_{i,j}\) 那么有:
发现 取了 1 个的方案
其实能够直接在 存在一列取了超过半数的方案
里面统计掉,由于必定是超过半数的。
没有取的方案直接不加上就行了。
而后就能够暴力枚举超过半数的材料是哪一个,进行DP。
设 \(f[i][j][k]\) 表示前 \(i\) 行,取了 \(j\) 个,其中超过半数的 \(x\) 取了 \(k\) 个( \(\Big\lfloor \dfrac{j}{2}\Big\rfloor <k\)),枚举到 \(pos\) 这道菜取了超过半数。
转移挺好想的,就是三种状况:
转移方程:
对于每一个 \(pos\) ,对答案的贡献就是 \(\sum_{i=0}^n\sum_{j=\lfloor i/2\rfloor+1}^i f[k][i][j]\) .
这样的复杂度是 \(\mathcal{O}(n^3m)\) 的,能获得 84 分的好成绩( 在考场上已经至关可观了……
而后考虑优化。发现合法状态只有 \(2\times k>j\) 的部分,也就是说你彻底不须要知道 \(j,k\) 的具体值,因此能够把状态搞成 \(2k-j\) ,省掉一维的枚举时间和空间。
那么方程就是:
(数组下标内的小括号表示根据原先的 \(j,k\) 定义,这个下标的值)
(注意,这里的合法状态指的是最终对答案有贡献的部分,从转移方程易知 \(2k\leq j\) 的部分仍是有用的,能够经过若干次 \(j-1\) 部分的转移贡献到合法状态里面去)
复杂度是 \(\mathcal{O}(n^2m)\) .
实现的时候注意减法取模……由于这个挂成 88 了qaq
//Author: RingweEH const int N=110,M=2010; const ll Mod=998244353; int n,m; ll a[N][M],g[N],sum[N],f[N][N<<1]; void add( ll &t1,ll t2 ) { t1=(t1+t2); if ( t1>Mod ) t1-=Mod; } int main() { n=read(); m=read(); for ( int i=1; i<=n; i++ ) for ( int j=1; j<=m; j++ ) a[i][j]=read(),add( sum[i],a[i][j] ); memset( g,0,sizeof(g) ); g[0]=1; for ( int i=1; i<=n; i++ ) for ( int j=i; j>=1; j-- ) add( g[j],g[j-1]*sum[i]%Mod ); ll ans=0; for ( int i=1; i<=n; i++ ) add( ans,g[i] ); for ( int pos=1; pos<=m; pos++ ) { memset( f,0,sizeof(f) ); f[0][n]=1; for ( int i=1; i<=n; i++ ) for ( int j=1; j<=n+i; j++ ) { f[i][j]=f[i-1][j]; add( f[i][j],f[i-1][j+1]*(sum[i]+Mod-a[i][pos])%Mod ); add( f[i][j],f[i-1][j-1]*a[i][pos]%Mod ); } for ( int i=n+1; i<=n*2; i++ ) add( ans,Mod-f[n][i]); } printf( "%lld\n",ans ); return 0; }
给定一个长为 \(n\) 的序列 \(a_i\) ,对于一组规模为 \(u\) 的数据,代价为 \(u^2\) .你须要找到一些分界点 \(1\leq k_1<k_2<...<n\) ,使得:
\(p\) 能够为 \(0\) 且此时 \(k_0=0\) .而后要求最小化::
求这个最小的值。
(数据生成方式见题面)
\(n\leq 4e7,1\leq a_i\leq 1e9,1\leq m\leq 1e5,1\leq l_i\leq r_i\leq 1e9,0\leq x,y,z,b_1,b_2\leq 2^{30}\)
难想好写的典型案例(其实也不难……)
一个显然的想法是DP分组。因为这道题跟组数没有关系,因此能够修改一下常规的式子。
设 \(f[i][j]\) 为对前 \(i\) 个进行分组,最后一组为 \([j+1,i]\) 的最小代价,\(sum[i]\) 为序列前缀和。
有方程:
复杂度为 \(\mathcal{O}(n^3)\) .问题出在上一个断点要一个一个枚举 \(k\) 获得。考虑如何加速这个过程。
注意到 “平方之和”必定比 “和的平方”要小。因此把最后一段拆成几段(在知足递增的状况下)答案必定不会变劣。
也就是说最优解的方案必定是合法的里面 最后一段最短 的一种。
那么这时候的 \(k\) 就是肯定的,数组就省掉了一维变成 \(f[i]\) .记录一个 \(las[i]\) 表示 \(f[i]\) 的方案中上一段的末尾。
方程就是:
复杂度 \(\mathcal{O}(n^2)\) 。这样已经实现了36分到64分的巨大飞跃(
然而对于 \(n\leq 4e7\) ,加上常数的话复杂度得是线性的……继续优化。😔
注意上面的 \(j\) 的条件式。
是否是清新可人的样子 你会发现若是一个 \(j\) 对于 \(i\) 知足上式,因为前缀和递增,显然对 \(i+1\) 也知足上式,所以可行决策点的范围必定是左端点为 \(1\) 的一个区间,且随着 \(i\) 的增大,这个区间的右端点递增(显然)。
咱们用一个函数 \(g(j)=2\times sum[j]-sum[las[j]]\) 来表示右式的值。根据题意,显然 \(j\) 的位置越靠右越优。
那么,若是有 \(j<j'\) 且 \(g(j)>g(j')\) ,\(j'\) 必定比 \(j\) 优,\(j\) 就是没用的了。
到这里,优化方式已经呼之欲出——单调队列!朴素想法就是在这个 \(j\) 单增 \(g(j)\) 单增的队列里面进行二分。可是这样还有一个 \(\log\) .
再考虑左式 \(sum[i]\) 的单调递增性质, 发现若是有一个点 \(j\) 对当前点 \(i\) 已经合法,能够进行转移了,那么 \(j\) 以前的点虽然能用,可是显然没有 \(j\) 好用,就能够丢掉了。因此每次从队头弹出直到留下最后一个合法点便可。
每一个点只会入队一次出队一次,均摊一下,转移复杂度就是 \(\mathcal{O}(1)\) 的,总复杂度 \(\mathcal{O}(n)\) . 数据范围诚不欺我
LOJ AC连接 给你们讲个笑话,这道题我同一份代码(去掉文件头了)在 ACWing 上重复提交四次能获得1次RE的好成绩(
卡空间就有点过度,不过考虑到 OJ 确实开不起这么大的空间也能够理解,就是出题人太恶心。(包括这个 __int128
的离谱操做)
//Author: RingweEH const int N=4e7+10,M=1e5+10; int n,las[N],p[M],l[M],r[M],typ,que[N]; ll a[N]; ll g( int x ) { return a[x]*2-a[las[x]]; } int main() { //freopen( "partition.in","r",stdin ); freopen( "partition.out","w",stdout ); n=read(); typ=read(); if ( typ==0 ) { for ( int i=1; i<=n; i++ ) scanf( "%lld",&a[i] ); } else { ll x,y,z; scanf( "%lld%lld%lld",&x,&y,&z ); int now=0,b[2],m; scanf( "%d%d%d",&b[0],&b[1],&m ); for ( int i=1; i<=m; i++ ) scanf( "%d%d%d",&p[i],&l[i],&r[i] ); for ( int i=1; i<=n; i++ ) { while ( p[now]<i ) now++; if ( i<=2 ) a[i]=b[i-1]%(r[now]-l[now]+1)+l[now]; else { b[0]^=b[1]^=(b[0]=(y*b[0]+x*b[1]+z)%(1<<30))^=b[1]; a[i]=b[1]%(r[now]-l[now]+1)+l[now]; } } } for ( int i=1; i<=n; i++ ) a[i]+=a[i-1]; int l=0,r=0; for ( int i=1; i<=n; i++ ) { while ( l<r && g(que[l+1])<=a[i] ) l++; las[i]=que[l]; while ( l<r && g(que[r])>=g(i) ) r--; que[++r]=i; } I128 ans=0; while ( n ) ans+=(I128)(a[n]-a[las[n]])*(a[n]-a[las[n]]),n=las[n]; int cnt=0; do { que[++cnt]=ans%10; ans/=10; }while ( ans ); do { printf( "%d",que[cnt] ); cnt--; }while ( cnt ); //fclose( stdin ); fclose( stdout ); // return 0; }
给定一棵 \(n\) 点的树,求单独删去每条边以后,分裂出的两个子树的重心编号和之和。(重心定义和简单性质自行阅读题面)
\(n\leq 299995\) .
考场骗分小能手狂喜(
发现前面 40 分的部分分彻底能够 \(\mathcal{O}(n^2)\) 暴力碾过去,枚举删边,而后 \(\mathcal{O}(n)\) DFS求一遍重心便可。
对于后面 15 分,有性质 \(A\) 也就是链。对于链,重心显然是找个中点就行了。
咳……这个要面向数据。
注意到题目里面对于这个部分分,钦定了 \(n=262143\) ,算一算就会发现是个满二叉树……其实满二叉树的根节点就是重心……
那么能够获得以下推论:
而后直接 \(\mathcal{O}(n)\) 枚举 \(\mathcal{O}(1)\) 计算就行了。
考虑重心的出现位置。有结论:
对于一个节点 \(u\) ,若是 \(n-siz[u]\leq \lfloor n/2\rfloor\) ,且 \(u\) 自己并不是重心,那么重心必定在 \(u\) 的重儿子里面。
这个挺显然的的吧。
而后就有一些显然的推论:(此处的 \(u\) 依然知足 \(n-siz[u]\leq\lfloor n/2\rfloor\) )
所以,重心必定在 root 向下的重链上,并且重链上自上往下,节点的 \(siz\) 递减。再结合数据范围获得合理猜想:复杂度 \(\mathcal{O}(n\log n)\) .
那么就能够考虑在重链上倍增。令 \(f[i][x]\) 表示以 rt 为根,节点 \(x\) 沿着重链往下走 \(2^i\) 步达到的节点。这样,求重心的时候就相似 LCA 同样,逆序枚举 \(i\) 往下跳就行了。
而后相似换根DP,二次扫描维护 \(f\) 数组和重儿子便可。
时间复杂度是 \(\mathcal{O}(n\log n)\) .
//Author: RingweEH const int N=3e5+10,K=25; struct edge { int to,nxt; }e[N<<1]; int head[N],tot=0,n,siz[N],f[N][K],son[N],fa[N]; ll ans; void ST_init( int x ) { for ( int i=1; i<K; i++ ) f[x][i]=f[f[x][i-1]][i-1]; } void calc( int x ) { int u=x; for ( int i=K-2; i>=0; i-- ) if ( f[u][i] && siz[f[u][i]]*2>=siz[x] ) u=f[u][i]; if ( siz[u]*2==siz[x] ) ans+=fa[u]; ans+=u; } void dfs( int u,int fat ) { fa[u]=fat; siz[u]=1; siz[0]=0; son[u]=0; for ( int i=head[u]; i; i=e[i].nxt ) { int v=e[i].to; if ( v==fat ) continue; dfs( v,u ); siz[u]+=siz[v]; if ( siz[v]>siz[son[u]] ) son[u]=v; //重儿子 } f[u][0]=son[u]; ST_init( u ); } void get_ans( int u,int fat ) { int mx1=0,mx2=0; siz[0]=0; for ( int i=head[u]; i; i=e[i].nxt ) { int v=e[i].to; if ( siz[v]>=siz[mx1] ) mx2=mx1,mx1=v; else if ( siz[v]>=siz[mx2] ) mx2=v; //最大和次大的儿子 } for ( int i=head[u]; i; i=e[i].nxt ) { int v=e[i].to; if ( v==fat ) continue; calc( v ); f[u][0]=(v==mx1) ? mx2 : mx1; ST_init( u ); siz[u]-=siz[v]; siz[v]+=siz[u]; calc( u ); fa[u]=v; get_ans( v,u ); siz[v]-=siz[u]; siz[u]+=siz[v]; //算完(u,v)以后撤销影响 } f[u][0]=son[u]; ST_init( u ); fa[u]=fat; } void add( int u,int v ) { e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; } int main() { //freopen( "centroid.in","r",stdin ); freopen( "centroid.out","w",stdout ); int T=read(); while ( T-- ) { memset( siz,0,sizeof(siz) ); memset( son,0,sizeof(son) ); memset( f,0,sizeof(f) ); memset( fa,0,sizeof(fa) ); ans=0; memset( head,0,sizeof(head) ); tot=0; n=read(); for ( int i=1,u,v; i<n; i++ ) u=read(),v=read(),add( u,v ); dfs( 1,0 ); get_ans( 1,0 ); printf( "%lld\n",ans ); } //fclose( stdin ); fclose( stdout ); return 0; }