日常の思维题
水博客太快乐了
RT
近日听的不少题目中,大多数不需要强大的码力、广阔的知识面或是过多的数学推倒,而是巧妙的思维。这些思维题很少有套路,往往只要一语便可让人恍然大悟。
尽管大部分题不会就是不会,怎么看也不会,但稍微积累一些至少也可以锻炼以下联想能力。。。
目前并不打算进行分类。。。(大部分都是 \(dp\) 和神仙题。。)
持续更新。。。
AT3857 [AGC020C] Median Sum
不难发现,对于全集的每一个子集,它对于全集的补集一定也是全集的子集。(废话)
于是,此题完结。
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define t first
#define w second
const int N=2010, M=4e6+10;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
bitset<M> ul;
int a[N], n, sum;
signed main(void){
n=read();
for(int i=1; i<=n; ++i) sum+=a[i]=read();
ul[0]=1;
for(int i=1; i<=n; ++i) ul|=(ul<<a[i]);
(++sum)>>=1;
while(!ul[sum]) ++sum;
printf("%d\n", sum);
return 0;
}
AT2705 [AGC019F] Yes or No
此题用到一个巧妙的转化。
设剩下的题中有 \(a\) 个题答案是 \(yes\) , \(b\) 个答案是 \(no\) 。
不难发现,若 \(a<b\) 则一定回答 \(no\) ,若 \(a>b\) ,则一定回答 \(yes\)。
若 \(a=b\) ,则回答什么均可,正确的概率是 \(\frac{1}{2}\) 。
考虑将剩余 \(yes\) 的个数变成变成 \(x\) 轴,将剩余 \(no\) 的个数变成 \(y\) 轴,那么问题便被转化成从点 \((n, m)\) 向点 \((0, 0)\) 走,每次只能向下或向左走。若在直线 \(y=x\) 下方,则每向左走一格会有 \(1\) 的贡献;若在直线 \(y=x\) 上方,则每向下走一格会有 \(1\) 的贡献;若在直线 \(y=x\) 上,则会有 \(\frac{1}{2}\) 的贡献。
不难发现,如果忽略在直线上的贡献,那么任意一种走的方法都会造成 \(max(n, m)\) 的贡献。
接下来只要求有多少种走法经过直线 \(y=x\) ,枚举直线上的每一个点即可,将求出的方案数除以 \(2\) 便是贡献,因为求的是期望,所以要除以总方案数,即 \(\binom{n+m}{n}\),最后加上 \(max(n, m)\) 即可。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10, mod=998244353;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int mul[N], inv[N], inv1[N], ans;
inline void pre(){
mul[0]=mul[1]=inv[0]=inv[1]=inv1[0]=inv1[1]=1;
for(int i=2; i<N; ++i){
mul[i]=1ll*mul[i-1]*i%mod;
inv1[i]=1ll*(mod-mod/i)*inv1[mod%i]%mod;
inv[i]=1ll*inv[i-1]*inv1[i]%mod;
}
}
inline int C(int n, int m) { return 1ll*mul[n]*inv[m]%mod*inv[n-m]%mod; }
inline int invC(int n, int m) { return 1ll*inv[n]*mul[m]%mod*mul[n-m]%mod; }
int n, m;
signed main(void){
n=read(), m=read(); pre();
if(n<m) n^=m, m^=n, n^=m;
for(int i=1; i<=m; ++i)
(ans+=1ll*C(n+m-i-i, n-i)*C(i<<1, i)%mod)%=mod;
ans=1ll*ans*invC(n+m, n)%mod*inv1[2]%mod;
printf("%d\n", (ans+n)%mod);
return 0;
}
AT2673 [AGC018D] Tree and Hamilton Path
考虑树上的一条边怎样才能实现利益最大化。若一条边连接两个点 \(u, v\) ,那么这条边最多会对答案做 \(2*min(siz_u, siz_v)\) 次贡献,即每个点一进一出。此时发现,最长哈密顿路径不好求,但是因为有每个点一进一出的性质,使得最长哈密顿回路很好求(每条边的贡献都是最大的)。
考虑求出哈密顿回路,在删去回路种最短的一条边。
考虑如何构造出一种哈密顿回路的方案。若随便选一个点当根,每次从它的一棵子树跳到另一棵子树,那么若这个点有一棵子树大于 \(\frac{n}{2}\) ,那么一定有边是在它子树内部连的,因此只需找出重心即可。再找到回路中最短的边,减去即是答案。
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define t first
#define w second
const int N=1e5+10, INF=0x3f3f3f3f;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n;
vector<pair<int, int> > l[N];
int siz[N], mx[N], rt, minn;
ll ans;
void find(int u, int fa){
siz[u]=1;
for(pair<int, int> v : l[u]){
if(v.t==fa) continue;
find(v.t, u); siz[u]+=siz[v.t];
mx[u]=max(mx[u], siz[v.t]);
ans+=1ll*v.w*min(siz[v.t], n-siz[v.t])*2;
}
mx[u]=max(mx[u], n-siz[u]);
if(mx[u]<mx[rt]) rt=u;
}
signed main(void){
n=read(); int x, y, z;
for(int i=1; i<n; ++i){
x=read(), y=read(), z=read();
l[x].push_back(make_pair(y, z));
l[y].push_back(make_pair(x, z));
}
mx[0]=INF; find(1, 0);
bool flag=0; minn=INF;
for(pair<int, int> v : l[rt])
if(mx[v.t]==mx[rt]) flag=1, ans-=v.w;
else minn=min(minn, v.w);
printf("%lld\n", flag ? ans : ans-minn);
return 0;
}
P5789 [TJOI2017]可乐
因为博主矩阵一直学的很菜,所以记录一下此题。。。
不难发现此题要求的是走 \(1\) 到 \(t\) 步后,从一号点到各个点的方案数,可以使用类似等比序列的方式求解,但是不够优。
因此我们大力发扬人类智慧,考虑改变矩阵的构造方法,可以将矩阵专门增加一维用来统计答案,将每一位的答案都转移到这一维上即可,这样只要正常矩阵快速幂便可以求得答案。。
code
#include<bits/stdc++.h>
using namespace std;
const int N=103, mod=2017;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, m, t, ans1;
struct mat{
int a[N][N], len;
friend mat operator * (const mat &x, const mat &y){
mat c; c.len=x.len; memset(c.a, 0, sizeof c.a);
for(int i=1; i<=c.len; ++i) for(int k=1; k<=c.len; ++k) for(int j=1; j<=c.len; ++j)
c.a[i][j]+=x.a[i][k]*y.a[k][j]%mod;
for(int i=1; i<=c.len; ++i) for(int j=1; j<=c.len; ++j)
c.a[i][j]%=mod;
return c;
}
}a, ans;
signed main(void){
n=read(), m=read(); int x, y;
ans.len=a.len=n+1;
for(int i=1; i<=m; ++i){
x=read(), y=read();
if(x==y) continue;
a.a[x][y]=a.a[y][x]=1;
}
for(int i=1; i<=n; ++i) a.a[i][n+1]=1;
for(int i=1; i<=n+1; ++i) ans.a[i][i]=1, a.a[i][i]=1;
t=read();
while(t){
if(t&1) ans=ans*a;
t>>=1; a=a*a;
}
for(int i=1; i<=n+1; ++i) (ans1+=ans.a[1][i])%=mod;
printf("%d\n", ans1);
}
AT2389 [AGC016E] Poor Turkeys
最早巨佬 \(Dky\) 讲过的一道题,当时就不会。。。
然后模拟赛烤出来,还是不会。。。
后来在杂题中看到,看了题解才又明白。。。
要求有多少个点对在结束时还存活,如果要按时间顺序求的话,状态过多,并不是很好做,因此考虑时间倒流。
假如要使点 \(i\) 存活下来,若出现 \((i, x)\),那么必须吃掉点 \(x\) ,因为一个点只能被吃掉一次,在这之前点 \(x\) 就必须被保护起来,此时若出现 \((x, y)\) ,因为 \(x\) 已经被保护起来,必须吃掉点 \(y\) ,则在次之前 \(y\) 也必须被保护起来。
不难发现,为了保护一个点 \(i\) ,需要将一堆点保护起来,然后让他们(除了 \(i\) )依次赴死,(好残忍。。。),不妨设这个点集为 \(S_i\) ,如果此时出现 \(a, b\) ,使得 \(a, b \in S_i\) ,则点 \(i\) 不可能活到最后,若只有 \(a \in S_i\) ,就将 \(b\) 也加入 \(S_i\) 。
判断点对 \((u, v)\) 是否能同时存活,只要满足这两个点能存活到最后且 \(S_u \cap S_v\) 为空即可。
因为 \(S_i\) 中的每个点是为了保证 \(S_i\) 中的另一个点不过早的被吃掉存在的,而一个点只能被吃一次,所以两个集合交集不为空则一定无法同时存活。
code
#include<bits/stdc++.h>
using namespace std;
const int N=510, M=1e5+10;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, m;
bitset<N> s[N];
int x[M], y[M], ans;
bool g[N];
int main(void){
n=read(), m=read();
for(int i=1; i<=m; ++i) x[i]=read(), y[i]=read();
for(int i=1; i<=n; ++i){
s[i][i]=1; bool a, b;
for(int j=m; j; --j){
a=s[i][x[j]], b=s[i][y[j]];
if(a&&b) { g[i]=1; break; }
if(a) s[i][y[j]]=1;
if(b) s[i][x[j]]=1;
}
}
for(int i=1; i<=n; ++i) for(int j=i+1; j<=n; ++j)
if(!g[i]&&!g[j]&&(s[i]&s[j]).none()) ++ans;
printf("%d\n", ans);
return 0;
}
CF258D Little Elephant and Broken Sorting
并不是很擅长期望题。。。
所以貌似是找了个比较板比较套路的题。。。
对于期望题中常出现的逆序对数量期望,不妨从逆序对的本质进行考虑,设 \(f_{i,j}\) 表示 \(i\) 大于 \(j\) 的概率,最终答案即为 \(\displaystyle \sum_{i=1}^n \sum_{j=i+1}^n f_{i, j}\) 。
考虑如何转移 ,不难发现,对于每一次交换 \(x, y\) ,均有
\(f_{x,y}=f_{y,x}=0.5\) ,因为一个排列中不存在相同的数。
\(\forall i \ne x,i \ne y, f_{x, i}=1-f_{i, x}\),这个貌似很好理解。。。
\(\forall i \ne x,i \ne y, f_{i, x}=0.5(f_{i, x}+f_{i,y})\),貌似也很好理解。。。
所以貌似是状态比较难想,只要知道了状态转移就很好想了。。。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, m, a[N];
double ans, f[N][N];
signed main(void){
n=read(), m=read(); int x, y;
for(int i=1; i<=n; ++i) a[i]=read();
for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j)
f[i][j]=(a[i]>a[j]);
while(m--){
x=read(), y=read();
for(int i=1; i<=n; ++i){
f[i][x]=f[i][y]=0.5*(f[i][x]+f[i][y]);
f[x][i]=f[y][i]=(1.0-f[i][x]);
}
f[x][y]=f[y][x]=0.5;
}
for(int i=1; i<=n; ++i) for(int j=i+1; j<=n; ++j)
ans+=f[i][j];
printf("%.9lf\n", ans);
return 0;
}
[CEOI2015 Day1]卡尔文球锦标赛
来自编译错误中欧 \(oi\) 的高质量 \(dp\) 。
此题最难的部分在于意识到这是个 \(dp\) (乍一看像个数学题。。。),并设计出状态。
分析题目的大意:
一个序列 \(a\) 合法当且仅当 \(a_1=1\) 且 \(\forall i ,a_i \le max_{j=1}^{i-1} a +1\) ,现在给出一个长度为 \(n\) 的序列,求它在所有长度为 \(n\) 的合法序列中字典须排第几。
只需求出所有字典序小于给出序列的合法序列个数即可。
理解题意后考虑设计状态,若不考虑给出的序列,那么某个位置能填的数只于这个位置前的最大值有关,因此不妨设 \(f_{i,j}\) 表示前 \(i\) 项序列中,最大值为 \(j\) 的合法序列有多少,但是因为有给出序列的限制,所以不妨仿照数位 \(dp\) ,再加一维 \(0/1\) 表示是否紧贴上界即可。
设计出状态后转移就简单多了:
令 \(mx_i = max_{j=1}^i a\)
\(f_{i, j, 0}=j*f_{i-1,j,0}+f_{i-1, j-1, 0}\)
\(f_{i, mx_i, 1}=f_{i-1, mx_{i-1}, 1}\)
\(f_{i, mx_{i-1}, 0}+=(a_i-1)*f_{i-1, mx_{i-1}, 1}\)
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=10010, mod=1e6+7;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, a[N], mx[N];
ll f[2][N][2], ans;
signed main(void){
n=read();
for(int i=1; i<=n; ++i) mx[i]=max(mx[i-1], a[i]=read());
f[1][1][1]=1;
for(int i=2; i<=n; ++i){
for(int j=1; j<=i; ++j)
f[i&1][j][0]=(f[(i&1)^1][j-1][0]+j*f[(i&1)^1][j][0])%mod;
f[i&1][mx[i]][1]=f[(i&1)^1][mx[i-1]][1];
(f[i&1][mx[i-1]][0]+=(a[i]-1)*f[(i&1)^1][mx[i-1]][1]%mod)%=mod;
}
for(int i=1; i<=n; ++i) (ans+=f[n&1][i][0])%=mod;
printf("%lld\n", ++ans);
return 0;
}
[CEOI2016]kangaroo
真·神级 \(dp\)
状态设计简直非人。。。
要求有多少 \(n\) 的排列 \(a\) 满足以 \(s\) 开头,以 \(t\) 结尾,且 \(\forall i\) :
\(\begin{cases}
a_i>a_{i+1} , if \ a_{i-1}<a_i
\\
a_i<a_{i+1} , if \ a_{i-1}>a_i
\end{cases}\)
一个不知道怎么才能想到的状态,设 \(f_{i,j}\) 表示填了 \(i\) 个,将前 \(i\) 个数分成 \(j\) 段,任意一段中相邻三个数大小关系满足如上要求。
考虑如何转移:
\(\begin{cases} f_{i,j}=f_{i-1,j-1}*(j-[i>s]-[i>t])+f_{i-1,j+1}*j, i\ne s \wedge i\ne t \\ f_{i,j}=f_{i-1,j-1}+f_{i-1,j}, i=s \vee i=t \end{cases}\)
若 \(i \ne t \& i \ne s\) ,那么每次可以将这个数当成新的一段插入到序列中,如果 \(s,t\) 已经被插入则不能再插入到序列首或序列尾,也可以将这个数用来将两个区间合并到一起。
若 \(i=t | i=s\),那么这个数只能插入到序列首或尾,可以另起一段,也可以与原来的段合并。
为什么合并是对的,可以用归纳法加以证明,这里不加以赘述。。。就是懒。。
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e3+10, mod=1e9+7;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, s, t;
ll f[N][N];
signed main(void){
n=read(), s=read(), t=read();
f[1][1]=1;
for(int i=2; i<=n; ++i) for(int j=1; j<=i; ++j){
if(i==s||i==t) f[i][j]=(f[i-1][j]+f[i-1][j-1])%mod;
else f[i][j]=(f[i-1][j+1]*j%mod+f[i-1][j-1]*(j-(i>s)-(i>t))%mod)%mod;
}
printf("%lld\n", f[n][1]);
return 0;
}
CF1442D Sum
神仙结论题。。。
此题有这样一个结论,如果在两个数组 \(a,b\) 中都选择了一部分,那么肯定不如全选 \(a\) 或全选 \(b\) 优。
证明如下,不妨设 \(a\) 当前选到第 \(i\) 个, \(b\) 当前选到第 \(j\) 个,若 \(a_i > b_j\) ,因为数组都是单增的,所以不妨不选 \(b_j\) 而是选择 \(a_{i+1}\) ,最终可以得到,将 \(a\) 数组全选是更优的答案。
有上述结论可以推出,所有选择的数组中,最多只有一个没有被全选,其余都是全选。
这样问题便被转化为强制不选某个物品进行背包,可以通过玄学分治解决。
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=3010;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, k;
vector<int> a[N];
ll w[N], v[N], ans;
vector<ll> f;
void solve(int l, int r){
if(l==r){
int t=0; ll sum=0;
for(int v : a[l]){
++t; sum+=v;
if(k<t) return;
ans=max(ans, sum+f[k-t]);
}
return;
}
int mid=(l+r)>>1; vector<ll> tmp=f;
for(int i=l; i<=mid; ++i) for(int j=k; j>=v[i]; --j)
f[j]=max(f[j], f[j-v[i]]+w[i]);
solve(mid+1, r); f=tmp;
for(int i=mid+1; i<=r; ++i) for(int j=k; j>=v[i]; --j)
f[j]=max(f[j], f[j-v[i]]+w[i]);
solve(l, mid);
}
signed main(void){
n=read(), k=read();
for(int i=1; i<=n; ++i){
int x=v[i]=read(), y;
while(x--){
w[i]+=y=read();
a[i].push_back(y);
}
}
f.resize(k+1); solve(1, n); printf("%lld\n", ans);
return 0;
}
[AGC052B] Tree Edges Xor
又一道神仙题。
仔细想想也不是特别神仙。。。是题解写的太艹了。。。。
首先考虑把边权转化到点权上去,满足每条边的权值等于两个端点权值的异或和,不难发现这实际上就是做前缀和(差分),这样每次对一条边的操作本质上就是交换两个点的点权
于是求出初始树和目标树的点权,分别记为 \(a, b\) ,只要判断这两个点权集合是否本质相等即可。但是如果将集合内所有数都同时异或一个定植,点权会改变然而所得到的边权不变。
此时问题转换为求是否有 \(X\) 满足 \(a_i \oplus X = b_j\) ,若这个式子满足,则一定有 \(\oplus_{i=1}^n (a_i \oplus X) = \oplus_{i=1}^n b_i\),由 \(n\) 是奇数可得, \(X = \oplus a_i \oplus b_i\) 。
于是将 \(b\) 内每个数异或 \(X\) ,再判断 \(a, b\) 是否一一对应即可。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, a[N], b[N];
struct edge { int t, w1, w2; };
vector<edge> l[N];
void dfs(int u, int f){
for(edge v : l[u]){
if(v.t==f) continue;
a[v.t]=a[u]^v.w1;
b[v.t]=b[u]^v.w2;
dfs(v.t, u);
}
}
signed main(void){
n=read();
for(int i=1, u, v, x, y; i<n; ++i){
u=read(), v=read(), x=read(), y=read();
l[u].push_back((edge) { v, x, y });
l[v].push_back((edge) { u, x, y });
}
dfs(1, 0);
for(int i=1; i<=n; ++i) b[0]^=a[i]^b[i];
for(int i=1; i<=n; ++i) b[i]^=b[0];
sort(b+1, b+1+n); sort(a+1, a+1+n);
for(int i=1; i<=n; ++i)
if(a[i]!=b[i]) { puts("NO"); return 0; }
puts("YES"); return 0;
}
[COCI2019-2020#3] Sob
又是 \(CEOI\) 的题。。。不过这题貌似不是特别难。。。
然而不难我也不会做。。。
首先题中告诉我们此题一定有解。
所以就不证明了。。。
考虑如何构造一组解。
不难发现若 \(a \& b = a\) ,一定有 \(a-1 \& b-1=a-1\) 。
对于每次配对,我们肯定希望 \(a\) 尽量大, \(b\) 尽量小。
因此枚举 \(a\) ,找到最小的 \(b\) 配对,然后依次将 \(a-i, b-i\) 配对即可。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, m;
signed main(void){
n=read(), m=read();
for(int i=n-1, lst=m, j=m; ~i; lst=++j){
while((i&j)!=i) ++j;
for(int k=j; k>=lst; --k) printf("%d %d\n", i--, k);
}
return 0;
}
P3672 小清新签到题
简单 \(dp\) 。 这是否代表着思维稍有提升?
和考试题比起来这题太小清新了。。。
看到题目中要求逆序对,那么有一个比较显然的 \(dp\) ,设 \(f_{i, j}\) 表示长度为 \(i\) ,包含 \(j\) 个逆序对的序列有多少,转移显然,且可以用前缀和优化,时间复杂度 \(O(nx)\) ,\(x\) 与 \(n^2\) 同级,因此这一部分的复杂度为 \(O(n^3)\) 。
此时要求字典序第 \(k\) 小的序列,那么较为套路地大力枚举每一位选什么就可以了。
考虑现在选第 \(i\) 位上的数,枚举到这一位上选 \(j\) ,在之后的序列中共有 \(x\) 个逆序对,若此时 \(k < f_{n-i, x}\) ,那么当前位不能选 \(j\) ,令 \(k\) 减去 \(f_{n-i,x}\) ,\(x\) 减一,\(j\) 加一即可。
注意到 \(f\) 可能会炸掉 \(long \ long\) ,而 \(k\) 最大是 \(10^{13}\) ,因此当 \(f\) 过大时令它等于 \(10^{13}\) 即可。
难得自己想出来 \(dp\) 题。 然而这题卡空间。。。
不开 \(long \ long\) 见祖宗。。。
P4699 [CEOI2011]Teams
又是 \(dp\) ,我怕是永远也做不出来了。。。
summary
None
博主太菜了。。。找不到思维题就找了不少水题。。。