题解:ABC200 ~ 204
abc200
D
二进制枚举子集+抽屉原理
简单的抽屉原理
在 \(201\) 个序列中必定有两个序列模 \(200\) 同余。
二进制枚举前 \(8\) 个元素的子集即可。
std::unordered_map<int,std::vector<int>> mp;
inline void solve()
{
int n; fin >> n;
std::vector<int> a(n);
for(int i = 0;i < n;i++) fin >> a[i];
int m = std::min(n,8);
for(int i = 1;i < (1<<m);i++)
{
int sum = 0;
std::vector<int> tmp;
for(int j = 0;j < m;j++)
if(i>>j & 1)
sum = (sum+a[j])%200,tmp.emplace_back(j+1);
if(mp[sum].size())
{
fout << "Yes\n";
fout << mp[sum].size() << ' ';
for(int x : mp[sum])
fout << x << ' ';
fout << '\n';
fout << tmp.size() << ' ';
for(int x : tmp)
fout << x << ' ';
fout << '\n';
return ;
}else mp[sum] = tmp;
}
fout << "No\n";
}
E
组合数学+模拟
注意到参数和的上限仅为 \(3N\) ,我们选择枚举参数和,
当前参数和所能达到的方案数可以使用插板法求出。
当我们枚举到了答案的参数和,我们就可以通过枚举 \(i\),从而得到j的上下限。
直到答案已经不能再逼近,我们就可以通过 \(i\) 和 \(j\) 以及参数和得到参数 \(k\) 。
#define int long long
int f(int x){if(x<=2)return 0;return(x-1)*(x-2)/2;}
inline void solve()
{
int n,k; fin >> n >> k;
for(int s = 3;s <= n*3;s++)
{
int t = f(s)-3*f(s-n)+3*f(s-2*n)-f(s-n*3);
if(k > t){k -= t;continue;}
for(int i = 1;i <= n;i++)
{
int mnj = std::max(s-i-n,1ll),
mxj = std::min(s-i-1,n);
if(mnj > mxj) continue;
int tot = mxj - mnj + 1;
if(k > tot){k -= tot;continue;}
int j = mnj + k - 1;
fout << i << ' ' << j << ' ' << s-i-j << '\n';
return ;
}
}
}
abc201
D
博弈论+DP
博弈 DP,奇状态下为 A 考虑,偶状态下为 B 考虑,状态为当前执子人落后另一位的分数.
const int N = 2e3+10;
char a[N][N];
int f[N][N],b[N][N];
inline void solve()
{
int n,m; fin >> n >> m;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
{
fin >> a[i][j];
if(a[i][j] == '-')
b[i][j] = -1;
else
b[i][j] = 1;
f[i][j] = inf;
}
f[n][m] = 0;
for(int i = n;i >= 1;i--)
for(int j = m;j >= 1;j--)
f[i-1][j] = std::min(-(f[i][j]+b[i][j]),f[i-1][j]),
f[i][j-1] = std::min(-(f[i][j]+b[i][j]),f[i][j-1]);
if(f[1][1] >= 1)
fout << "Aoki";
else if(f[1][1] <= -1)
fout << "Takahashi";
else
fout << "Draw";
}
E
位运算+组合数学
位运算,先预处理出根节点到每个点的路径异或和,此时答案变为给一个数组,求每两对i,j之间的异或值的和。
考虑拆位,只有当前位不同的情况下才会产生贡献,即为 \(x \times (n-x) \times 2^i\) (\(x\) 为对所有元素,当前位为 \(1\) 的数量)。
最后对每一位产生的贡献求和即可。
#define int long long
#define pii std::pair<int,int>
const int M = 1e9+7;
const int N = 2e5+10;
std::vector<pii> E[N];
int dis[N],f[64];
void dfs(int u,int fa)
{
for(auto [v,w] : E[u])
{
if(fa == v) continue;
dis[v] = dis[u] ^ w;
dfs(v,u);
}
}
inline void solve()
{
int n; fin >> n;
for(int i = 1;i <= n-1;i++)
{
int u,v,w; fin >> u >> v >> w;
E[u].emplace_back(v,w);
E[v].emplace_back(u,w);
}
dfs(1,0);
for(int i = 1;i <= n;i++)
for(int j = 0;j <= 60;j++)
if((dis[i]>>j) & 1ll)
f[j] = (f[j]+1) % M;
int ans = 0;
for(int i = 0;i <= 60;i++)
{
ans += (((((1ll<<i)%M)*f[i])%M*((n-f[i])%M)))%M;
ans %= M;
}
fout << ans << '\n';
}
abc202
D
递归+组合数学
设计函数 \(query(a,b)\) 为 \(a\) 个 \(0\) \(b\) 个 \(1\) 可产生的方案数(记忆化搜索)。
设计函数 dfs,从高到低位递推,尽可能的放a,若放a后,后面元素能产生的方案数 \(< k\) ,则当前位改为放 \(b\)。
直到没有 \(a\) 或 \(b\) 可放了为止。
#define int long long
int f[35][35];
int query(int a,int b)
{
if(!a||!b) return 1;
if(!f[a-1][b]) f[a-1][b] = query(a-1,b);
if(!f[a][b-1]) f[a][b-1] = query(a,b-1);
return f[a][b-1]+f[a-1][b];
}
void dfs(int a,int b,int k)
{
if(!a)
{
fout << std::string(b,'b');
return ;
}
else if(!b)
{
fout << std::string(a,'a');
return ;
}
int qa = query(a-1,b);
if(qa >= k)
{
fout << 'a';
dfs(a-1,b,k);
}
else
{
fout << 'b';
dfs(a,b-1,k-qa);
}
}
inline void solve()
{
int a,b,k; fin >> a >> b >> k;
dfs(a,b,k);
}
E
欧拉序/DFS序+线段树
树上问题,可以使用线段树维护 dfs 序来暴力求解,也可以使用欧拉序。
由于子节点区间一定在祖先节点区间内,所以我们可以使用欧拉序+二分来确定满足条件的子节点个数。
(可以使用二分是因为欧拉序插入深度集合的顺序一定是单调递增的)。
const int N = 2e5+10;
int tot,in[N],out[N];
std::vector<int> d[N];
std::vector<int> E[N];
void dfs(int u,int dep)
{
in[u] = tot++;
d[dep].emplace_back(in[u]);
for(int v : E[u]) dfs(v,dep+1);
out[u] = tot++;
}
inline void solve()
{
int n,q; fin >> n;
for(int i = 2;i <= n;i++)
{
int x; fin >> x;
E[x].emplace_back(i);
}
dfs(1,0);
fin >> q;
while(q--)
{
int a,b; fin >> a >> b;
fout << std::lower_bound(d[b].begin(),d[b].end(),out[a]) - std::lower_bound(d[b].begin(),d[b].end(),in[a]) << '\n';
}
}
abc203
D
二分+二维前缀和
经典 Trick,我们使用二分答案,每次 check 时根据 mid 值将大于 \(mid\) 的元素记为 \(1\),反之记为 \(0\)。
对新二维数组进行二维前缀和预处理,然后枚举每个大小为 k * k 的子区间的右下角端点。
此时我们可以 \(O(1)\) 统计子区间大于 \(mid\) 的元素的个数,若当前子区间选作 \(mid\) 作为中位数太小时,返回 True,
也就是我们可以进一步缩小答案,若没有区间能满足,则返回 False,让答案区间的左端点向右移动,每次返回
True 时记录答案,最后输出即可。
#define int long long
const int N = 810;
int a[N][N],s[N][N],n,k;
bool check(int x)
{
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
s[i][j] = (a[i][j] > x) + s[i][j-1] + s[i-1][j] - s[i-1][j-1];
for(int i = k;i <= n;i++)
for(int j = k;j <= n;j++)
{
int cnt = s[i][j] - s[i-k][j] - s[i][j-k] + s[i-k][j-k];
if(cnt <= k*k/2)
return 1;
}
return 0;
}
inline void solve()
{
fin >> n >> k;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
fin>>a[i][j];
int l = 0,r = 1e9+10,ans = 0;
while(l <= r)
{
int mid = (l+r)>>1;
if(check(mid))
{
ans = mid;
r = mid-1;
}else
l = mid+1;
}
fout << ans << '\n';
}
E
模拟
显然我们对行数枚举会T,我们选择枚举黑子,准备一个集合,从上到下枚举黑子,若当前黑子可以被转移,则在集合内插入该黑子的位置。
若该黑子的位置有白子在上方,则从集合内删除,显然插入的优先级比删除更高,所以先删除,后插入。
inline void solve()
{
int n,m; fin >> n >> m;
std::set<int> s;
std::map<int,std::vector<int>> mp;
for(int i = 1;i <= m;i++)
{
int x,y; fin >> x >> y;
mp[x].emplace_back(y);
}
s.insert(n);
for(auto i : mp)
{
std::vector<int> era,ins;
for(auto j : i.second)
{
if(s.count(j)) era.push_back(j);
if(s.count(j-1) || s.count(j+1))
ins.push_back(j);
for(int x : era) s.erase(x);
for(int x : ins) s.insert(x);
}
}
fout << s.size() << '\n';
}
F
DP + 二分
我们注意到,操作数的量级是 \(log\) 级别的,所以我们使用 \(f(i,j)\) 表示对于前i个草,操作j次,最少拔草的数量。
显然,我们要先对草的高度进行排序,从低到高 dp ,每次可以从两个状态处转移,分别是拔草,以及选择操作。
拔草的转移方程显然,操作的转移方程需要知道对于当前草,拔除它会影响的范围,
我们在排序后可以选择使用二分将每个草能够影响到的位置给预处理出来,在 dp 时直接调用即可。
最后,我们枚举终状态在不同操作数下的值,由于我们需要最小化操作次数,所有我们遍历到约束内最小的操作次数后记录输出即可。
#define all(a) a.begin()+1,a.end()
#define int long long
const int N = 2e6+10;
int f[N][40];
inline void solve()
{
int n,k; fin >> n >> k;
std::vector<int> a(n+1),low(n+1);
for(int i = 1;i <= n;i++) fin >> a[i];
sort(all(a));
for(int i = 1;i <= n;i++)
{
int u = std::lower_bound(all(a),(a[i]/2)+1)-a.begin();
low[i] = u-1;
}
for(int i = 1;i <= n;i++)
for(int j = 0;j <= 31;j++)
{
f[i][j] = f[i-1][j]+1;
if(j)
f[i][j] = std::min(f[i][j],f[low[i]][j-1]);
}
int ans = LLONG_MAX,res = 0;
for(int i = 0;i <= 30;i++)
if(ans > i && f[n][i] <= k)
ans = i , res = f[n][i];
fout << ans << ' ' << res << '\n';
}
abc204
D
01背包
我们发现我们要尽可能缩小大值与小值之间的差距,我们考虑让小值尽可能大。
设置一个容量为总元素和除以二的 01 背包,跑一边 01 背包即可得到小值。
最后使用总元素和减去小值即为大值。
#define int long long
const int N = 1e6+10;
int a[N],f[N];
inline void solve()
{
int n; fin >> n;
int tot = 0;
for(int i = 1;i <= n;i++) {fin >> a[i];tot+=a[i];}
int m = tot/2;
for(int i = 1;i <= n;i++)
for(int j = m;j >= a[i];j--)
f[j] = std::max(f[j],f[j-a[i]]+a[i]);
fout << tot - f[m] ;
}
E
均值不等式 / 求导 + 最短路
我们需要最小化题目中给的式子,考虑均值不等式或者求导,笔者选择了使用求导法(不会均值不等式)。
根据分式的求导法则,得出 \(min(f(t)) = sqrt(d_i)-1\) 。
贪心的,我们在当前 \(t\) 大于等于 \(sqrt(d_i)-1\) 时选择直接走,
小于 \(sqrt(d_i)-1\) 时选择等待到 \(t = sqrt(d_i)-1\) 时再走。
使用该方法我们跑一遍堆优化过的 dijkstra 算法,时间复杂度可以达到 \(O(nlogn)\)。
using ll = long long;
#define int long long
#define inf 0x3f3f3f3f3f3f3f3fll
const int N = 2e5+10;
struct e{int v,x,y;double sq;};
std::vector<e> E[N];
struct node{int u,d;};
bool operator<(node a,node b){return a.d > b.d;}
std::priority_queue<node> q;
int dis[N],vis[N];
int f(int t,int x,int y,double sq)
{
if(sq-1 < t) return t+x+y/(t+1);
if(ceil(sq)-1 < t) return ll(sq)+y/ll(sq)+x-1;
return std::min(ll(sq)+y/ll(sq),ll(ceil(sq))+y/ll(ceil(sq)))+x-1;
}
void dij()
{
memset(dis,0x3f,sizeof(dis));
dis[1] = 0;q.push({1,0});
while(!q.empty())
{
auto [u,d] = q.top();
q.pop();
if(vis[u]) continue;
vis[u] = 1;
for(auto [v,x,y,sq] : E[u])
{
int t = f(d,x,y,sq);
if(t < dis[v])
{
dis[v] = t;
q.push({v,dis[v]});
}
}
}
}
inline void solve()
{
int n,m; fin >> n >> m;
while(m--)
{
int u,v,x,y; fin >> u >> v >> x >> y;
if(u != v)
E[u].push_back({v,x,y,std::sqrt(y)}),
E[v].push_back({u,x,y,std::sqrt(y)});
}
dij();
if(dis[n] == inf) fout << -1 << '\n';
else fout << dis[n] << '\n';
}
F
状压DP + 矩阵加速
一道矩阵加速状压DP的好题,
我们可以先考虑行数为 1 的情况,我们可以发现递推式(不作证明,很好理解),
\(f(1) = 1\)
\(f(2) = 2\)
\(f(i) = f(i-1) + f(i-2)\)
再来考虑行数不为 1 的情况,
由于 \(N \le 8\) ,考虑状态压缩,状态数 $\le 2^8 $。
我们对于每一个状态预处理出可以产生的方案数,使用 \(res\) 数组来记录,
对于每一段连续的空位,都会产生相应的贡献,上面我们已经讨论过横着的情况,竖着也是同理(甚至都不用预处理,自己手写一下就行)。
然后,我们发现若我们直接进行递推,复杂度显然不对。
因为我们的递推是线性的,所以我们可以将状态转移的操作记录进矩阵内,使用矩阵快速幂进行对数级别的递推即可。
最后让初始矩阵与转移矩阵相乘即可得到答案。
(注意细节,比如矩阵乘法的顺序之类的。。。)
#define int long long
const int N = 70;
const int mod = 998244353;
int res[N];
const int f[]={1,1,2,3,5,8,13,21};
int n,m,l;
void add(int &x,int y) {x+=y;if(x>=mod) x-=mod;}
struct matrix
{
int f[N][N];
matrix() {memset(f,0,sizeof(f));}
friend matrix operator *(const matrix &a,const matrix &b)
{
matrix res;
for(int i=0;i<m;i++)
for(int k=0;k<m;k++)
for(int j=0;j<m;j++)
add(res.f[i][j],a.f[i][k]*b.f[k][j]%mod);
return res;
}
}G,A;
matrix qpow(matrix n,int k)
{
matrix res = n;
for(;k;n=n*n,k>>=1)
if(k&1)
res=res*n;
return res;
}
inline void solve()
{
fin >> n >> l;
m = 1<<n;
for(int i = 0;i < m;i++)
{
int x = 0;
res[i] = 1;
for(int j = 0;j < n;j++)
if((i>>j)&1) x++;
else res[i] *= f[x],x = 0;
res[i] *= f[x];
}
for(int i = 0;i < m;i++)
for(int j = 0;j < m;j++)
if((i|j) == m-1)
G.f[i][j] = res[i&j];
A.f[m-1][0] = 1;
A = qpow(G,l-1)*A;
fout << A.f[m-1][0] << '\n';
}

浙公网安备 33010602011771号