双向搜索 --- meet in the middle 算法
主要思想:把一个搜索分成两个搜索,分别搜索,最后将结果合并。
暴力搜索的复杂度往往是指数级的,而改用 meet in the middle 算法后复杂度的指数可以减半。
P4799 [CEOI2015 Day2] 世界冰球锦标赛
题目大意:n张球赛门票对应价格(n <= 40),身上有m块钱(m <= 1e18),问有多少种方案看球赛。
最开始考虑暴搜和背包,都会T。
最后还得是双向搜索,把n分成左右两块进行搜索,分别记录所有方案所花费的钱。
最后合并答案:枚举一边的所有方案花费,从另一边找到一个花费,若是左+右<=m,则是一种可行方案。
vector<ll>lf, rg;
int flag;
ll a[50] = { 0 };
ll m;
void dfs(int i, int n, ll sum) {
if (i == n) {
if (flag) rg.push_back(sum);
else lf.push_back(sum);
return;
}
if (sum + a[i + 1] <= m)
dfs(i + 1, n, sum + a[i + 1]);
dfs(i + 1, n, sum);
}
void solve() {
int n;
cin >> n >> m;
rep(i, 1, n) cin >> a[i];
sort(a + 1, a + 1 + n); //排序优化后面的搜索
dfs(0, n / 2, 0);
flag = 1;
dfs(n / 2, n, 0);
sort(rg.begin(), rg.end()); //对要进行二分搜索的数组排序
ll ans = 0;
for (ll x : lf) { //
int id = upper_bound(rg.begin(), rg.end(), m - x) - rg.begin();
ans += id;
}
cout << ans << endl;
}
P5691 [NOI2001] 方程的解数
k1*x1^p1 + k2*x2^p2 +...+ kn*xn^pn = 0,x为未知数,由于(n <= 6, x <= 150),又给了6s,首先想到的肯定是暴搜,但是也还是会TLE。
折半搜索优化,怎么用呢,把原式转化为k1*x1^p1 + k2*x2^p2 +...+ k(n/2)*x(n/2)^p(n/2)=-k(n/2+1)*x(n/2+1)^p(n/2+1)...- k(n - 1)*x(n - 1)*p(n - 1) - kn*xn^pn。
把n分成两次搜,第一次用map存好每一个值,第二次查看有没有该值的相反数即可。
int ans;
int flag; //标记是第一次还是第二次
int n, x;
int k[7], p[7];
map<int, int>mp;
int ksm(int a, int b) {
int ret = 1;
while (b) {
if (b & 1) {
ret *= a;
}
a *= a;
b >>= 1;
}
return ret;
}
void dfs(int number, int nowber, int sum) {
if (number == nowber) {
if (flag) ans += mp[-sum]; //第二次
else mp[sum]++; //第一次
return;
}
for (int i = 1; i <= x; i++) { //枚举x的取值
dfs(number + 1, nowber, sum + k[number + 1] * ksm(i, p[number + 1]));
}
}
void solve() {
cin >> n >> x;
rep(i, 1, n) cin >> k[i] >> p[i];
dfs(0, n / 2, 0); // 1
flag = 1;
dfs(n / 2, n, 0); // 2
cout << ans << endl;
}
算是初步了解了一下这个算法。
题解
牛客多校5 -- H
题目大意:给定n个奶酪(n <= 200),大小为a,质量为b,有m次机会去拿这些奶酪(m <= 1e5),每次最多拿满sz大小的奶酪(sz递增)。
在奶酪前时,只能拿掉当前奶酪或打洞过去,被打了洞的奶酪不能拿。问m次后最多拿多少质量的奶酪。
思路:当m > n时,只需要考虑后面n次拿奶酪即可,在第每个奶酪前时,只有两种情况,拿或不拿,就是背包问题,
所以易得到dp转移方程为 dp[j][k]=max(dp[j][k], dp[j - 1][k - a[i]] + b[i])。i为第几个奶酪,j是第几次拿,k是第j次拿了多少。
int a[205], b[205];
int n, m;
void solve() {
int ans = 0;
cin >> n >> m;
rep(i, 1, n) cin >> a[i] >> b[i];
vector<int>sz(m + 1);
rep(i, 1, m) cin >> sz[i];
if (m > n) {
sz.erase(sz.begin() + 1, sz.end() - n);
m = n;
}
vector<vector<int> >dp(m + 1, vector<int>(201));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int k = sz[j]; k >= a[i]; k--) { // 背包
dp[j][k] = max(dp[j][k], dp[j][k - a[i]] + b[i]);
}
}
for (int j = 1; j <= m - 1; j++) { //处理出下次拿的初始情况
for (int k = sz[j]; k >= 0; k--)
dp[j + 1][0] = max(dp[j + 1][0], dp[j][k]);
}
}
for (int i = 0; i <= 200; i++) {
ans = max(dp[m][i], ans);
}
cout << ans << endl;
}
E. Tracking Segments
题目大意:给你一个长度为n的数组(初始都为0),m个数段,q次更新,每次更新为一个下标,把数组中该下标上的值改为1,每次更新的下标不同。好数段:1的个数严格大于0的个数。问最少在第几次更新后会有至少一个好数段。若q次更新后仍没有好数段,则输出-1。
做法:二分 + 前缀和
二分答案 : 二分最小更新次数得答案。
int inp[N];
PII ds[N];
int n, m, q;
bool check(int mid) {
vector<int>sum(n + 1, 0);
for (int i = 1; i <= mid; i++) { //初始化,前面操作过的也要记上
sum[inp[i]] = 1;
}
for (int i = 1; i <= n; i++) sum[i] += sum[i - 1]; //前缀和,每个区间的1的个数
for (int i = 0; i < m; i++) {
int x = ds[i].first, y = ds[i].second;
if (sum[y] - sum[x - 1] > ((y - x + 1) / 2)) {
// cout << "l r : " << x << ' ' << y << endl;
return true;
}
}
return false;
}
void solve() {
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> ds[i].first >> ds[i].second;
}
cin >> q;
for (int i = 1; i <= q; i++) {
cin >> inp[i];
}
int l = 1, r = q + 1;
while (l < r) { //二分
int mid = l + r >> 1;
// cout << "l r : " << l << ' ' << r << endl;
if (check(mid)) r = mid;
else l = mid + 1;
}
if (r == q + 1) l = -1;
cout << l << endl;
}
牛客萌新赛5 --- 幂运算
如果要求解 a ^ b % p , b 的数值非常大的时候, 就需要用到欧拉函数来降幂。
这题需要求解的是2^(2^n)%p, n的范围是le6, 所以要进行降幂。
ll ksm(ll a, ll b, ll p) {
ll ans = 1;
while (b) {
if (b & 1) {
ans *= a;
ans %= p;
}
a *= a;
a %= p;
b >>= 1;
}
return ans;
}
ll euler_phi(ll n) { //欧拉函数
ll m = sqrtl(n);
ll ans = n;
for (ll i = 2; i <= m; i++)
if (n % i == 0) {
ans = ans / i * (i - 1);
while (n % i == 0) n /= i;
}
if (n > 1) ans = ans / n * (n - 1);
return ans;
}
void solve() {
ll n, p;
cin >> n >> p;
ll eul = euler_phi(p);
ll m = ksm(2, n, eul);
cout << ksm(2, m, p) << endl;
}
牛客萌新赛5 - F
题目大意:给定n个点(x, y, v坐标和扩散速度),m次查询(在t时刻有多少个点)(n,m,t <= 1e3)
每一个点像圆一样扩散,两个点相遇后会合并为一个点,合并不影响扩散。
思路:计算出每两个点的相遇时间,并记录在每一个时刻都有哪两个相遇,按照时间先后合并相遇的点,得出在该时刻,少了多少个点。
做法:遍历n^2,计算出所有两个点相遇时间,并存好每个时间点有哪些点相遇,遍历0-1000时刻,用并查集进行合并操作,每合并一次,该时刻减少一个点,最后前缀和得出所有时刻有多少个点。
struct node {
int x, y, v;
}a[1005];
int Dtim(node x, node y) { //计算两个墨点相遇时间
double dis = sqrt(sqr(x.x - y.x) + sqr(x.y - y.y));
if (x.v + y.v == 0) return 1001; //遇不到
double t0 = dis / (x.v + y.v);
return ceil(t0);
}
int t[1005];
int pre[1005];
vector<PII>zz[1005];
int find(int f) {
if (pre[f] == f) return f;
return pre[f] = find(pre[f]);
}
void solve() {
int n, m;
cin >> n;
rep(i, 1, n) {
cin >> a[i].x >> a[i].y >> a[i].v;
pre[i] = i;
}
t[0] = n;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) { //把每两个相遇的时间算出来
//把在这个时间相遇的两个点存起来
int t0 = Dtim(a[i], a[j]);
if (t0 <= 1000 && t0 >= 0) //把相遇时间在需要考虑范围的两个墨点存起来
zz[t0].push_back({ i, j });
}
}
for (int i = 0; i <= 1000; i++) { //遍历每一个时间点
for (auto x : zz[i]) { //用并查集把相遇的墨滴合并
int u = x.first, v = x.second;
u = find(u), v = find(v);
if (u != v) {
t[i]--;
pre[u] = v;
}
}
}
for (int i = 1; i <= 1000; i++) { //前缀和处理
t[i] += t[i - 1];
if (t[i] == 1) break; //最终状态是1
}
cin >> m;
while (m--) {
int inp;
cin >> inp;
cout << (t[inp] == 0 ? 1 : t[inp]) << endl;
}
}
继续加油!!!