∞ \infty ∞ 年没打过比赛了,周末打了一场,来写一下总结。
T1
题面(有原题不截图了)。
典型的二分答案类问题(带限制求最值),现在问题就是怎么写 check 函数。
然后 check 里面的那个贪心我是真服了,大致就是说我们把原数列从大到小排序,然后把前几个数作为每个组别的开端,接下来的数要在满足题目条件的情况下尽可能的被塞进去,那我们选择这个数能被塞进去的最小的那个组别就行,因为比这个数更大的组别能选的范围更广,选它们自然更不优,而更小的你又塞不下去。然后这个过程可以做到
O
(
n
)
O(n)
O(n)。于是总时间复杂度为
O
(
n
log
n
)
O(n\log n)
O(nlogn)。
当然更极端一点就是:假设我们要分成 x x x 组,我们可以直接规定第 i i i 个数必须被分到 i m o d x i\bmod x imodx 组里去,如果塞不进去就直接跳过。正确性我不会证明,反正就这样吧。
代码:
#include<bits/stdc++.h>
#define int long long
#define code using
#define by namespace
#define plh std
code by plh;
namespace fastio
{
inline int read()
{
int z=0,f=1;
char c=getchar();
if(c==EOF)
{
exit(0);
}
while(c<'0'||c>'9')
{
if(c==EOF)
{
exit(0);
}
if(c=='-')
{
f=-1;
}
c=getchar();
}
while(c>='0'&&c<='9')
{
z=z*10+c-'0';
c=getchar();
}
return z*f;
}
inline void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
static int top=0,stk[106];
while(x)
{
stk[++top]=x%10;
x/=10;
}
if(!top)
{
stk[++top]=0;
}
while(top)
{
putchar(char(stk[top--]+'0'));
}
}
inline void write(string s)
{
for(auto i:s)
{
putchar(i);
}
}
}
using namespace fastio;
int n,k,rr,a[1000006],mn[1000006],num[1000006];
bool check(int x)
{
if(x==0)
{
return true;
}
for(int i=1;i<=x;i++)
{
mn[i]=1e18;
num[i]=0;
}
for(int i=1;i<=n;i++)
{
if(mn[i%x==0?x:i%x]-a[i]>=rr)
{
mn[i%x==0?x:i%x]=a[i];
num[i%x==0?x:i%x]++;
}
}
for(int i=1;i<=x;i++)
{
if(num[i]<k)
{
return false;
}
}
return true;
}
signed main()
{
n=read(),k=read(),rr=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
}
sort(a+1,a+n+1,greater<int>());
int l=0,r=n/k,mid,ans;
while(l<=r)
{
mid=l+r>>1;
if(check(mid))
{
ans=mid;
l=mid+1;
}
else
{
r=mid-1;
}
}
write(ans);
return 0;
}
另一种方法的代码洛谷题解区里有,而且那种方法似乎更科学一点,正确性可证明,建议大家还是看另外一种做法。
T2
题面。
水题,考试的时候没考虑完整被坑了。
首先我们看下面的部分分,我们会发现 k = 3 k=3 k=3 给的分最多,而其余几种情况给的分都很少。说明其他几种应该很好做,我们先考虑其余几种情况。
-
k
=
0
k=0
k=0:此时答案肯定是
YES,随便排都行。 -
k
=
1
k=1
k=1:此时我们把建筑高度从小到大排序,然后从上往下、从左往右依次建建筑,这样就能保证每栋楼至少都有一个朝左的方向。所以答案为
YES。 -
k
=
2
k=2
k=2:此时还是按照
k
=
1
k=1
k=1 的情况建立,容易发现每个建筑都至少有朝上和朝左两个方向。所以答案为
YES。 - k = 4 k=4 k=4:此时所有建筑高度一定要全部一样,不然肯定会有建筑缺少至少一个方向。这个判断一下就行。
现在回到 k = 3 k=3 k=3 的情况。我们要思考一种一定合法的构造方法,从而判断是否成立。
我们会发现:对于四个角上的建筑,它们天生就有两个方向,我们只需要再找一个方向就行。因此在角上放高度最小的建筑肯定更好。那么因为所有建筑的高度都大于等于它,所以我们可以直接忽略这个最低的建筑,那么就可以以同样的方式继续构造。可以我们现在要角上的建筑多出一个方向,那就只能在它的附近,甚至是它这一排再安排跟它同样高度的建筑。这样这一排所有的建筑正好都能满足条件。
因此我们现在的问题就变成了:对所有建筑按高度从小到大排序后,每次可以选择一整行或一整列全填充这一种建筑,问能否把整张图全部填充完毕。很容易发现这里我直接默认了这两个条件之间是充要关系,至于为什么也是能证明的。简单证明一下。
首先证明充分性,也就是说如果我按照上面的方法构造出来了,那么原图一定有至少一种构造方法。这一看不就是一句废话吗。再证明必要性。假设按照我的方法构造不出来,那么最小的建筑肯定就不是放在最边上,因此我们容易想到如果最小的建筑放在中间,那么就必须强行构造出来三个方向,保证这三个方向上的建筑高度都是最小高度。也就是长这样:

标红的那一块是高度最小的建筑,然后那三条绿色的柱子就是我们随便选的三个方向。
假设红色方块上面的建筑都比它高,那么下面那个绿色矩形上的所有建筑都还要再找两个方向,也就是左右方,于是最后你会发现下面那一整块就全变成最小高度的建筑了,这和我们之前的构造方法并没有什么区别。
如果说红色方块有四个方向,也就是这样:

那么对于绿色矩形上的建筑,每个建筑需要再找至少一个方向,如果只找一个方向的话就会变回上面的那种情况,所以每个建筑都必须再找两个方向,那再考虑这两个方向上的建筑,又要再重复上面的过程,随后整张图就全都是最小高度的建筑了,这和我们之前的构造方法没什么区别。
因此必要性也是正确的。
所以说如果原图有构造方法,那么就必定有我给出的那种构造方法。我们可以直接用来判定。
然后我们可以直接使用 dfs 解决这个问题,为了防止时间复杂度飞起,我们需要用记忆化搜索。
当然你也可以优化一下:我们很容易发现你每次相当于要删掉一个 L 型的东西,于是我们可以设你删的 L 型的东西的两个宽,列方程,然后每次只需要解方程就行,而且这样会去除掉很多一定错误的情况,相当于一个剪枝了。
代码(没加剪枝):
#include<bits/stdc++.h>
#define int long long
#define code using
#define by namespace
#define plh std
code by plh;
namespace fastio
{
inline int read()
{
int z=0,f=1;
char c=getchar();
if(c==EOF)
{
exit(0);
}
while(c<'0'||c>'9')
{
if(c==EOF)
{
exit(0);
}
if(c=='-')
{
f=-1;
}
c=getchar();
}
while(c>='0'&&c<='9')
{
z=z*10+c-'0';
c=getchar();
}
return z*f;
}
inline void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
static int top=0,stk[106];
while(x)
{
stk[++top]=x%10;
x/=10;
}
if(!top)
{
stk[++top]=0;
}
while(top)
{
putchar(char(stk[top--]+'0'));
}
}
inline void write(string s)
{
for(auto i:s)
{
putchar(i);
}
}
}
using namespace fastio;
struct build{
int x,y;
}a[10006];
int n,m,kk,f[1006][1006];
bool dfs(int x,int y,int pos,int s)
{
if(pos==m&&s==0)
{
return true;
}
if(f[x][y]!=-1)
{
return f[x][y];
}
if(!s)
{
pos++;
s=a[pos].y;
}
f[x][y]=0;
if(s>=x)
{
f[x][y]|=dfs(x,y-1,pos,s-x);
}
if(s>=y)
{
f[x][y]|=dfs(x-1,y,pos,s-y);
}
return f[x][y];
}
signed main()
{
memset(f,-1,sizeof(f));
n=read(),kk=read(),m=read();
for(int i=1;i<=m;i++)
{
a[i].x=read(),a[i].y=read();
}
if(kk<=2)
{
write("YES");
return 0;
}
if(kk==4)
{
if(m>1)
{
write("NO");
}
else
{
write("YES");
}
return 0;
}
sort(a+1,a+m+1,[&](build x,build y)
{
return x.x<y.x;
});
if(dfs(n,n,1,a[1].y))
{
write("YES");
}
else
{
write("NO");
}
return 0;
}
代码(加了剪枝):
#include<bits/stdc++.h>
#define int long long
#define code using
#define by namespace
#define plh std
code by plh;
namespace fastio
{
inline int read()
{
int z=0,f=1;
char c=getchar();
if(c==EOF)
{
exit(0);
}
while(c<'0'||c>'9')
{
if(c==EOF)
{
exit(0);
}
if(c=='-')
{
f=-1;
}
c=getchar();
}
while(c>='0'&&c<='9')
{
z=z*10+c-'0';
c=getchar();
}
return z*f;
}
inline void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
static int top=0,stk[106];
while(x)
{
stk[++top]=x%10;
x/=10;
}
if(!top)
{
stk[++top]=0;
}
while(top)
{
putchar(char(stk[top--]+'0'));
}
}
inline void write(string s)
{
for(auto i:s)
{
putchar(i);
}
}
}
using namespace fastio;
struct build{
int x,y;
}a[10006];
int n,m,kk,f[1006][1006];
bool dfs(int x,int y,int pos)
{
if(pos==m+1)
{
return true;
}
if(f[x][y]!=-1)
{
return f[x][y];
}
f[x][y]=0;
for(int j=0;j<=n&&a[pos].y-x*j>=0;j++)
{
if(y-j==0)
{
if(a[pos].y%x==0)
{
f[x][y]|=dfs(0,0,pos+1);
}
}
else
{
if((a[pos].y-x*j)%(y-j)==0)
{
f[x][y]|=dfs(x-(a[pos].y-x*j)/(y-j),y-j,pos+1);
}
}
}
return f[x][y];
}
signed main()
{
memset(f,-1,sizeof(f));
n=read(),kk=read(),m=read();
for(int i=1;i<=m;i++)
{
a[i].x=read(),a[i].y=read();
}
if(kk<=2)
{
write("YES");
return 0;
}
if(kk==4)
{
if(m>1)
{
write("NO");
}
else
{
write("YES");
}
return 0;
}
sort(a+1,a+m+1,[&](build x,build y)
{
return x.x<y.x;
});
if(dfs(n,n,1))
{
write("YES");
}
else
{
write("NO");
}
return 0;
}
T3
题面。
首先我们很容易想到把字符串从 1 1 1 开始编码,然后我们就能得出它每次洗牌后的位置的规则:
p o s → { p o s × 2 − 1 p o s ≤ n 2 p o s × 2 − n p o s > n 2 pos\to\begin{cases}pos\times2-1&pos\le\frac{n}{2}\\pos\times2-n&pos\gt\frac{n}{2}\end{cases} pos→{pos×2−1pos×2−npos≤2npos>2n
其中 n n n 指牌数,也就是题目中给的 64 k 64^k 64k。
然后我们稍加研究不难发现:每张牌最多只会跳 6 ⋅ k 6\cdot k 6⋅k 次就会回到原来的位置。又因为 k k k 很小,所以每次询问我们是可以暴力跳的。
于是我们直接用 long long 算出当前的位置在哪,然后暴力跳下一个位置,回到原位置了就停下,看看要跳多少步才能跳到终点。这样能获得 40pts。
然后我们再用一下高精度,实现好一点时间复杂度就是 O ( m k 2 ) O(mk^2) O(mk2),能过 70pts(我常数太大被卡了,还要压位才能拿到 70pts)。
现在我们考虑正解,当然你也可以尝试压位做到 O ( m k 2 w ) O(\frac{mk^2}{w}) O(wmk2) 看看能不能飞过去,反正作者的高精度常数有点大,飞不过去(而且哪个出题人搬题的时候把时限卡到了 500ms 啊!!!)。
这里我们需要转换一下编号:我们最初是 1-index,现在我们换成 0-index,也就是从 0 开始,那么我们的规则就变成了这样:
p o s → { p o s × 2 p o s < n 2 p o s × 2 − n + 1 p o s ≥ n 2 pos\to\begin{cases}pos\times2&pos\lt\frac{n}{2}\\pos\times2-n+1&pos\ge\frac{n}{2}\end{cases} pos→{pos×2pos×2−n+1pos<2npos≥2n
然后我们把编号放在二进制下看,那么第一种操作就是在后面补了一个 0 0 0,第二种操作就是先补了一个 0 0 0,然后把最高位的那个 1 1 1 去掉,再把末尾那个 0 0 0 变成 1 1 1。我们不难发现:这两种操作都相当于把二进制循环移位了一下,也就是把头放到了尾部。
因此我们可以先把编号的二进制编码保存下来,然后在后面再拼上这个编码,于是我们的问题就变成了找到一段区间 [ i , i + 6 ⋅ k − 1 ] [i,i+6\cdot k-1] [i,i+6⋅k−1] 要和目标二进制编码一样。问这个 i i i 是多少。比较两个字符串是否相同这种问题明显交给哈希做啊。于是时间复杂度: O ( m k ) O(mk) O(mk)。
代码:
#include<bits/stdc++.h>
#define int long long
#define code using
#define by namespace
#define plh std
code by plh;
namespace fastio
{
inline int read()
{
int z=0,f=1;
char c=getchar();
if(c==EOF)
{
exit(0);
}
while(c<'0'||c>'9')
{
if(c==EOF)
{
exit(0);
}
if(c=='-')
{
f=-1;
}
c=getchar();
}
while(c>='0'&&c<='9')
{
z=z*10+c-'0';
c=getchar();
}
return z*f;
}
inline void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
static int top=0,stk[106];
while(x)
{
stk[++top]=x%10;
x/=10;
}
if(!top)
{
stk[++top]=0;
}
while(top)
{
putchar(char(stk[top--]+'0'));
}
}
inline void write(string s)
{
for(auto i:s)
{
putchar(i);
}
}
}
using namespace fastio;
int B=3;
int k,m;
unsigned int pw,h1[12006],h2[6006];
int F(char c)
{
if(c=='#')
{
return 0;
}
if(c=='$')
{
return 1;
}
if(c>='0'&&c<='9')
{
return c-'0'+2;
}
if(c>='A'&&c<='Z')
{
return c-'A'+12;
}
if(c>='a'&&c<='z')
{
return c-'a'+38;
}
}
string change(string s)
{
string ss="";
for(int i=0;i<k;i++)
{
int id=F(s[i]);
for(int j=5;j>=0;j--)
{
ss+=to_string((id>>j)&1);
}
}
return ss;
}
signed main()
{
k=read(),m=read();
int len=k*6;
pw=1;
for(int i=1;i<=len;i++)
{
pw=pw*B;
}
while(m--)
{
string s1,s2;
cin>>s1>>s2;
s1=change(s1);
s2=change(s2);
s1=s1+s1;
s1=" "+s1,s2=" "+s2;
for(int i=1;i<=len*2;i++)
{
h1[i]=h1[i-1]*B+(s1[i]-'0'+1);
}
for(int i=1;i<=len;i++)
{
h2[i]=h2[i-1]*B+(s2[i]-'0'+1);
}
int ans=0;
bool fl=false;
for(int i=0;i<=len;i++)
{
if(h1[i+len]-h1[i]*pw==h2[len])
{
fl=true;
break;
}
ans++;
}
if(!fl)
{
write(-1);
}
else
{
write(ans);
}
putchar('\n');
}
return 0;
}
T4
题面(原题是 CF 的,但是找不到了):

题面很短,但是概期 DP 的题一般都不好做。
首先我们考虑我们最终要求的式子的组合意义是什么。很容易想到 ∏ a i \prod a_i ∏ai 的最基本的组合意义就是指第 i i i 个盒子里面有 a i a_i ai 个球,从每个盒子里面选出一个球的总方案数有多少种。而每次操作实际上就像是往第 i i i 及后面的所有箱子里面都扔了 v v v 个球。
因此我们可以设 f i , j f_{i,j} fi,j 表示前 i i i 个箱子,有 j j j 次操作放进去的球被取了出来,于是我们可以转移:
如果当前盒子取出来的球是之前某一次操作放进去的球,那么总的方案数就是 f i + 1 , j = f i , j × ( a i + 1 + j × v ) f_{i+1,j}=f_{i,j}\times(a_{i+1}+j\times v) fi+1,j=fi,j×(ai+1+j×v)。
如果当前盒子取出来的球是某一个之前没有被取出来过的操作,那么可以得到 f i + 1 , j + 1 = f i , j × ( m − j ) × v × ( i + 1 ) f_{i+1,j+1}=f_{i,j}\times(m-j)\times v\times(i+1) fi+1,j+1=fi,j×(m−j)×v×(i+1),其中 ( m − j ) × v (m-j)\times v (m−j)×v 表示取出来的球是哪个, i + 1 i+1 i+1 表示我是从前面哪个箱子的操作中取出来的。
于是我们可以得到答案就是 ∑ i = 0 n f n , i n i \sum_{i=0}^n\frac{f_{n,i}}{n^i} ∑i=0nnifn,i。
代码:
#include<bits/stdc++.h>
#define int long long
#define code using
#define by namespace
#define plh std
code by plh;
namespace fastio
{
inline int read()
{
int z=0,f=1;
char c=getchar();
if(c==EOF)
{
exit(0);
}
while(c<'0'||c>'9')
{
if(c==EOF)
{
exit(0);
}
if(c=='-')
{
f=-1;
}
c=getchar();
}
while(c>='0'&&c<='9')
{
z=z*10+c-'0';
c=getchar();
}
return z*f;
}
inline void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
static int top=0,stk[106];
while(x)
{
stk[++top]=x%10;
x/=10;
}
if(!top)
{
stk[++top]=0;
}
while(top)
{
putchar(char(stk[top--]+'0'));
}
}
inline void write(string s)
{
for(auto i:s)
{
putchar(i);
}
}
}
using namespace fastio;
int n,m,v,a[5006],f[5006][5006];
const int mod=1e9+7;
int qpow(int x,int y)
{
int z=1;
while(y)
{
if(y&1)
{
z=z*x%mod;
}
x=x*x%mod;
y>>=1;
}
return z;
}
signed main()
{
n=read(),m=read(),v=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
}
f[0][0]=1;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
f[i+1][j]=(f[i+1][j]+f[i][j]*a[i+1]%mod+f[i][j]*j%mod*v%mod)%mod;
f[i+1][j+1]=(f[i+1][j+1]+f[i][j]*(m-j)%mod*v%mod*(i+1)%mod)%mod;
}
}
int ans=0;
for(int i=0;i<=n;i++)
{
ans=(ans+f[n][i]*qpow(qpow(n,i),mod-2)%mod)%mod;
}
write(ans);
return 0;
}

690

被折叠的 条评论
为什么被折叠?



