CF1854 题解
A
首先考虑只有非负的情况,次数完全可以接受 \(19\) 次,所以直接用 \(19\) 次做一次前缀和就可以保证单调不降了。
现在有了负数,考虑将负数变成正数,选出正数当中的最大值,然后用 \(a_i + a_i \to a_i\) 这样自增的方式让它的绝对值大于负数最大值,因为绝对值小于等于 \(20\) ,所以最多 \(5\) 次就可以达到。与其在一个负数上加很多次,显然不如先将正数加到 \(20\) 以上,再只加一次得到结果,但是我们只有 \(31\) 次,减去前缀和 \(19\) 次和这 \(5\) 次,我们还有 \(7\) 次,就是可以加 \(7\) 个负数,那么如果负数太多怎么办呢?
考虑到如果将整个数列取反,就相当于要构造一个不升序列,再 \(reverse\) 过来,就又变成了原问题,但是原来的正负数个数交换了,在上述情况 \(7\) 个负数以上的情况时,既然我们正数要自增 \(5\) 次,那么负数最大值一定大于正数最大值,所以我们翻转后无需自增,直接将新的负数加成正数就好了,次数是 \(19 + 12 = 31\) 次,这个 \(12\) 是因为上段情况不成立时原来的负数一定超过 \(8\) 个,那么新的负数一定小于等于 \(12\) 个,由此得证。
#include<bits/stdc++.h>
using namespace std;
int n,a[25],pa[25];
vector <pair<int,int> > pos;
inline void solve()
{
int maxpos = 0,minneg = 0,maxi = 0;
for(int i = 1;i <= n;i++) maxpos = max(maxpos,a[i]),minneg = min(minneg,a[i]);
if(maxpos == 0 && minneg != 0)
{
for(int i = 1;i <= 32;i++) pos.push_back(make_pair(1,1));
return;
}
for(int i = 1;i <= n;i++) if(maxpos == a[i]) maxi = i;
while(maxpos < -minneg) pos.push_back(make_pair(maxi,maxi)),a[maxi] *= 2,maxpos *= 2;
for(int i = 1;i <= n;i++) if(a[i] < 0) pos.push_back(make_pair(i,maxi)),a[i] += a[maxi];
for(int i = 2;i <= n;i++) if(a[i] < a[i - 1]) pos.push_back(make_pair(i,i - 1)),a[i] += a[i - 1];
}
int main()
{
int T;
cin>>T;
while(T--)
{
cin>>n;
for(int i = 1;i <= n;i++) cin>>a[i];
for(int i = 1;i <= n;i++) pa[i] = a[i];
solve();
if(pos.size() > 31)
{
pos.clear();
for(int i = 1;i <= n;i++) a[i] = -pa[i];
reverse(a + 1,a + n + 1);
solve();
cout<<pos.size()<<endl;
for(auto i : pos) cout<<n + 1 - i.first<<" "<<n + 1 - i.second<<endl;
}
else
{
cout<<pos.size()<<endl;
for(auto i : pos) cout<<i.first<<" "<<i.second<<endl;
}
pos.clear();
}
return 0;
}
B
考虑到一个关键性质:得分和摸牌其实是同一个东西,再加上我们摸到的牌一定是一个区间 \([1,k]\) ,我们假设最后摸了 \(k\) 张牌,那么答案就是 \(\sum_{i = 1}^{k}a_i - (k - 1)\)。相当于摸了 \(k\) 张牌就一定有 \(k - 1\) 个值被拿来摸牌。
所以我们只需要知道能否有一种方案,使得最后恰好摸了 \(k\) 张牌即可,假设 \(dp_i\) 为能否摸到 \(i\) 张牌,每次枚举 \(a_i\) ,有转移: \(dp_j = dp_j\ |\ dp_{j - a_i}\) 。
我们注意到 \(dp\) 值是一个 \(bool\) 类型,和 \(10^5\) 的数据、 \(3s\) 的时长。
考虑用 \(bitset\) 优化,每次将 \(dp\) 数组整体左移 \(a_i\) 再或一下即可。注意就算超过了 \(n\) 也要减掉,所以数组大小开两倍。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
bitset <N * 2> dp,able;
long long a[N],s[N],n;
int main()
{
cin>>n;
for(int i = 1;i <= n;i++) cin>>a[i],s[i] = a[i] + s[i - 1];
dp[1] = 1;able[1] = 1;
for(int i = 1;i <= n;i++)
{
dp |= (dp << a[i]);
able[i + 1] = dp[i + 1];
dp[i] = 0;
}
long long ans = 0;
for(int i = 1;i <= n;i++) if(able[i]) ans = max(ans,s[i] - i + 1);
for(int i = n + 1;i <= 2 * n;i++) if(dp[i]) ans = max(ans,s[n] - i + 1);
cout<<ans;
return 0;
}
C
期望 \(dp\) 题目,这题不好设状态,于是我们一个一个条件来。考虑没有“相撞”的答案,就相当于每个数到 \(m + 1\) 的距离之和。即 \(\sum_im + 1 - a_i\) 。由于有了相撞,我们多算了一些东西,考虑到相撞的概率只和 \(3\) 个元素有关:相撞的两个数和相撞位置,设数为 \(x,y\) ,位置为 \(z\) 。那么期望就要减去 \(p_{x,y,z} \times (m + 1 - z)\) 。
发现只有相邻的两个数字会相撞(不是的话可以等效)。所以考虑每一对数 \(a_i,a_{i + 1}\) 。设 \(f_{x,y}\) 表示两个数分别在 \(x,y\) 位置的概率,首先将 \(f_{a_i,a_{i + 1}}\) 设为 \(1\) 。然后由于只考虑两个数的移动,分别以 \(\frac 12\) 的概率将 \(f_{x,y}\) 转移到 \(f_{x + 1,y},f_{x,y + 1}\) 即可。从小到大枚举左端点,那么 \(f_{x,x}\) 就是两数在 \(x\) 处相撞的概率。时间复杂度 \(O(nm^2)\) 。
由于最后我们要将减掉的东西加起来,所以根据期望线性性,我们可以将每一对数一起处理,只用 \(dp\) 一遍,时间复杂度 \(O(m^2)\) 。
#include<bits/stdc++.h>
using namespace std;
const int N = 505,MOD = 1e9 + 7;
typedef long long ll;
ll a[N],n,m,dp[N][N];
inline ll ksm(ll base,ll pts)
{
ll ret = 1;
for(;pts > 0;pts >>= 1,base = base * base % MOD)
if(pts & 1)
ret = ret * base % MOD;
return ret;
}
int main()
{
cin>>n>>m;
ll sig = 0;
for(int i = 1;i <= n;i++) cin>>a[i],sig += (m + 1 - a[i]);
memset(dp,0,sizeof(dp));
for(int i = 1;i <= n - 1;i++) dp[a[i]][a[i + 1]] = 1;
ll ans = sig;
for(int i = 1;i <= m;i++)
{
ans = (ans - dp[i][i] * (m + 1 - i) % MOD + MOD) % MOD;
for(int j = i + 1;j <= m;j++)
{
ll iv = ksm(2,MOD - 2);
dp[i + 1][j] = (dp[i + 1][j] + iv * dp[i][j] % MOD) % MOD;
dp[i][j + 1] = (dp[i][j + 1] + iv * dp[i][j] % MOD) % MOD;
}
}
cout<<ans;
return 0;
}
D
题意就是求出 \(1\) 所在树的所有节点,考虑如果找出环上的每一个点,就可以询问其他所有点,如果 \(x\) 走 \(n\) 步可以到达环上的点,那么就在这棵树中。
首先,我们用一个二分法找到环上的第一个点,我们将已有的点集分为两部分,询问 \(1\) 是否可以走 \(n\) 步到达左半边,如果可以就取左半边,否则就取右半边。这样每次询问次数是 \(\lceil \log 500\rceil = 9\) 。
已知环上的一个点集 \(G\) ,我们有两种做法:
1.我们可以用上次询问的点 \(x\) ,观察它一步走到的是哪个点,这个点一定在环上,如果这个点已经被访问过,就说明已经找到了整个环,但是这样次数是 \(size \times 9\) 的,太多。
2.我们可以使用倍增,每次暴力扫描每个点,观察到目前点集 \(G\) 是否可以走 \(|G|\) 步到达。如果可以,要么是环上的前 \(|G|\) 个点,要么是树上的一些节点,但是树上的点最后还是要计入答案,所以不影响。这样的次数是 \(n - 1 + n - 2 + n - 4 + n - 8 + \dots\) ,还是太多。
我们发现,前一种方法适用于 \(|G|\) 很小的情况,后面适用于 \(|G|\) 很大的情况,于是整合这两个方法,就可以控制询问在 \(2000\) 次以内,计算得出第一种方法暴力找 \(63\) 个点时最优。最后询问每个点能否走 \(n\) 步到 \(G\) 即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 2005,p = 63;
int vis[N],n;
vector <int> G;
inline int q(int u,int k,vector <int> v)
{
cout<<"? "<<u<<" "<<k<<" "<<v.size()<<" ";
for(auto in : v) cout<<in<<" ";
cout<<endl;
int ret;
cin>>ret;
return ret;
}
inline int ask1()
{
vector <int> l,r,d;
for(int i = 1;i <= n;i++) d.push_back(i);
while(d.size() > 1)
{
l.clear();r.clear();
for(int i = 0;i < d.size() / 2;i++) l.push_back(d[i]);
for(int i = d.size() / 2;i < d.size();i++) r.push_back(d[i]);
d.clear();
if(q(1,n,l)) for(auto in : l) d.push_back(in);
else for(auto in : r) d.push_back(in);
}
return d[0];
}
inline int ask(int x)
{
vector <int> l,r,d;
for(int i = 1;i <= n;i++) d.push_back(i);
while(d.size() > 1)
{
l.clear();r.clear();
for(int i = 0;i < d.size() / 2;i++) l.push_back(d[i]);
for(int i = d.size() / 2;i < d.size();i++) r.push_back(d[i]);
d.clear();
if(q(x,1,l)) for(auto in : l) d.push_back(in);
else for(auto in : r) d.push_back(in);
}
return d[0];
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n;
G.push_back(ask1());
vis[G[0]] = 1;
for(int i = 2;i <= p;i++)
{
int ret = ask(G[i - 2]);
if(vis[ret] == 1) break;
else vis[ret] = 1,G.push_back(ret);
}
if(G.size() == p)
{
int shz = G.size();
while(1)
{
for(int i = 1;i <= n;i++)
{
if(vis[i]) continue;
if(q(i,shz,G)) G.push_back(i),vis[i] = 1;
}
shz *= 2;
if(shz > G.size()) break;
}
}
for(int i = 1;i <= n;i++)
{
if(vis[i]) continue;
if(q(i,n,G)) G.push_back(i),vis[i] = 1;
}
cout<<"! "<<G.size()<<" ";
for(auto i : G) cout<<i<<" ";
cout<<endl;
return 0;
}
E
考虑随机化,子序列中大于 \(30\) 的数最多只有一个,所以我们 \(rand\) 前 \(30\) 个数,要求 \(\leq 30\) ,然后求出 \(f_i\) ,表示子序列和等于 \(i\) 时的情况数。然后将 \(31 - 60\) 按照 \(f_{60 - i}\) 从大到小排序,然后只要 \(m\) 还大于 \(f_{60 - p_i}\) ,就在序列末尾加上一个 \(p_i\) 。如果最后凑到了 \(0\) ,就输出方案,不然就全部重来。考虑最终方案有很多种,所以玄学地可以 \(AC\) 。
#include<bits/stdc++.h>
using namespace std;
const int N = 105;
typedef long long ll;
ll m,dp[N],a[N],p[N],n,top;
inline int qr(int l,int r)
{
return l + rand() % (r - l + 1);
}
inline bool cmp(int x,int y){return dp[60 - x] > dp[60 - y];}
int main()
{
srand(time(NULL));
cin>>m;
if(m <= 60)
{
cout<<m<<endl;
for(int i = 1;i <= m;i++) cout<<"60 ";
return 0;
}
for(int i = 1;i <= 30;i++) p[i] = 30 + i;
while(1)
{
memset(dp,0,sizeof(dp));
dp[0] = 1;
n = 0;
top = qr(2,30);
for(int i = 1;i <= 60;i++)
{
a[++n] = rand() % top + 1;
if(dp[60] + dp[60 - a[n]] > m) {n--;break;}
for(int j = 60;j >= a[n];j--) dp[j] += dp[j - a[n]];
}
ll lft = m - dp[60];
sort(p + 1,p + 31,cmp);
for(int i = 1;i <= 30;i++)
while(n < 60 && lft >= dp[60 - p[i]]) a[++n] = p[i],lft -= dp[60 - p[i]];
if(!lft)
{
cout<<n<<endl;
for(int i = 1;i <= n;i++) cout<<a[i]<<" ";
return 0;
}
}
return 0;
}
F
咕。
标签:CF1854,int,题解,ll,back,60,push,dp From: https://www.cnblogs.com/TheLastCandy/p/17678246.html