用这个博客记录一下自己学习过程中遇到的问题和心得体会.
学习这本书时用的是oj是acwing,上面有几乎所有书上的题目.
二次更新记录一下要注意的题和一些心得:
3最短Hamilton路径(这题虽然很经典.不过是状压dp的经典题.)
4.起床困难综合征(这是运算顺序的问题,很多时候前往后算不对,后往前算可能就可能对了,尤其是贪心题.)
9奇怪的汉诺塔(思维题,递推一般都是搞脑子的题,多见能多开阔思路.)
10约数之和(分治很经典的题.可以拿来练分治)
11分形之城(分形很经典的题.还有旋转矩阵的知识.练它)
18七夕祭(问题分解+环形分割纸牌.问题分解的思想很重要(虽然很基础),环形分割纸牌感觉没有开拓很多思路,就是学习了一个很厉害的解法.)
22天才ACM(倍增练习题(也是很难的练习题),倍增是一个很厉害的算法,归并排序也是很厉害的一个算法.在需要多次用到排序的时候,归并排序的作用就很明显了.合并两个有序序列只需要O(n+m)的复杂度,代码难度也不小,可以练手)
23防晒(贪心经典题,很多题目都是这题引申出来的.提供一个可能不完全正确但很多用到的思路(只是一个类似的思路):一个物品,在全部人都能选的情况下(或者是满足全部某一维的情况),给一个要求最低的人,因为这样才能空出更多好的东西给挑剔的人选.)
26国王游戏(贪心的一种难题,感觉很难总结出什么.领项交换,微扰有些确实不好想.假设解的结构.还是思维要够开阔.多想)
27给树染色(这题和天才ACM一样,不仅思维难度大,代码难度也不小.我觉得属于开阔视野题.这种题目,和后面基础数据类型里面有几题有点像,数学归纳法.在具体做题的时候靠猜,靠感觉,证明是数学专业要干的事.)
29袭击(最近平面点对,不要太经典了.归并排序的妙用.分治练手题,练手题不代表简单题…)
31赶牛入圈(这题属于思维难度可能不是很大.但是代码难度确实不小…离散化很经典的题目.很好的练手题)
33士兵(思维题.观察解的结构我觉得也是很重要的一个能力,解的结构往往能给人很多做题的思路.不错的开阔思维题,)
35最大的和(很经典的我不会做的题目…数组转矩阵是真的很不擅长了…多练习吧.)
36数的进制转换(高精度练习题,短除法的妙用.)
37任务(也是能很好开阔思维的题目.因为题目的一些限制,有些条件可能会被弱化(这是挺重要的一种思路).练它)
0x01
1.a^b
快速幂模板题
所谓快速幂.是利用了数二进制的特点.后面倍增算法的思想也和这个有异曲同工之妙.二进制有什么特点呢?最大的一个特点就是它的每一位只有1和0,说明在该位上面,这位上的数只有存在和不存在两种可能.也就是说ab可以把它拆成ak1ak2…这里的k1k2是把b拆分成二进制后对应的次幂20,21这样子.每次都让a=a×a就可以拿到b的每一位对应的数了,如果b这一位上面的数是1.那么就让res = res×a. 要注意的是p可能是1所以在运算前要先让res=1%p;
代码
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int main(){
LL a,b,p;
cin >> a >> b >> p;
LL res = 1%p;
while(b){
if(b & 1) res = res*a%p;
b >>= 1;
a = a*a%p;
}
cout << res;
return 0;
}
2.64位整数乘法
也就是慢速乘,这里的abp范围都是在1e18内的.如果直接相乘能达到1e36.这是肯定会溢出的.根据快速幂的思想.我们一样把b拆成若干二进制组成的01串.表达式可以转换成(20+21…)*a. 所以每次a都只需要×2就可以了.这样就不会溢出了.
代码
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int main(){
LL a,b,p;
cin >> a >> b >> p;
LL res = 0;
while(b){
if(b&1) res = (res+a)%p;
b >>= 1;
a = a*2%p;
}
cout << res;
return 0;
}
3.最短Hamilton路径
这题是一道经典的状态压缩dp题. dp[i][j]表示当前以i这个点为终点.j表示所有的状态(和书上的的i,j顺序是反的).为什么可以这么表示呢?我们先考虑终点.终点是11111…n个1.表示经过了所有点以后以n这个点为终点的最优路径(为什么要加一个终点来限制可以画图深刻理解一下.).它肯定是由(假设n=5)11110这个状态加上一条1-4 -> 5 的边转移过来的.
说的很混乱,还是上代码吧.
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int w[21][21];
int dp[21][(1 << 21)];
int main(){
int n;
cin >> n;
for(int i=0;i<n;++i) for(int j=0;j<n;++j) cin >> w[i][j];
memset(dp,0x3f,sizeof(dp));
dp[0][1] = 0;
for(int i=1;i<(1 << n);++i){
for(int j=0;j<n;++j){
if((1 << j) & i){
for(int k=0;k<n;++k){
if(j == k) continue;
if((1 << k) & i){
dp[j][i] = min(dp[j][i],dp[k][(1 << j) ^ i] + w[k][j]);
}
}
}
}
}
cout << dp[n-1][(1 << n) - 1];
return 0;
}
4.起床困难综合征
位运算嘛.突出的就是一个拆位.一位一位的考虑如果0和1都可以让答案变成1那就选0不然只有1可以的话就选1.但是不能超过m.按道理说应该倒着推能保证正确.但是顺着推也能ac.
代码
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int n,m;
pii a[N];
bool calc(int x,int bit){
for(int i=0;i<n;++i){
int t = (a[i].second>>bit)&1;
if(a[i].first == 0) x &= t;
if(a[i].first == 1) x |= t;
if(a[i].first == 2) x ^= t;
}
return x;
}
int main(){
cin >> n >> m;
for(int i=0;i<n;++i){
string s;
cin >> s >> a[i].second;
if(s[0] == 'A') a[i].first = 0;
if(s[0] == 'O') a[i].first = 1;
if(s[0] == 'X') a[i].first = 2;
}
LL res = 0,tmp = 0;
for(int i = 30;i>=0;--i){
if(calc(0,i)) res += (1 << i);
else {
if(calc(1,i)&&(1 << i) + tmp <= m) {
tmp+=(1 << i);res+=(1 << i);}
}
}
cout << res;
return 0;
}
0x02
5.递归实现指数型枚举
6.递归实现组合型枚举
7.递归实现排列型枚举
这三题没啥好说的.主要是练习简单的剪枝和回溯.
5 代码
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int n;
vector<int> v;
void dfs(int k){
if(k == n + 1){
for(auto x:v) cout << x << " ";
cout << endl;
return;
}
dfs(k+1);
v.pb(k);
dfs(k+1);
v.pop_back();
}
int main(){
cin >> n;
dfs(1);
return 0;
}
6 代码
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int n,m;
vector<int> v;
void dfs(int k){
if(n-k+1 < (m-v.size())) return;
if(k == n + 1){
for(auto x:v) cout << x << " ";
cout << endl;
return;
}
v.pb(k);
dfs(k+1);
v.pop_back();
dfs(k+1);
}
int main(){
cin >> n >> m;
dfs(1);
return 0;
}
7 代码
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int n;
int v[10];
bool vis[10];
void dfs(int k){
if(k == n+1){
for(int i=1;i<=n;++i) cout << v[i] << " ";
cout << endl;
return;
}
for(int i=1;i<=n;++i){
if(vis[i]) continue;
v[k] = i;
vis[i] = 1;
dfs(k+1);
vis[i] = 0;
}
}
int main(){
cin >> n;
dfs(1);
return 0;
}
8.费解的开关
一道很经典的题目.需要先处理一下问题. 枚举每一个位置复杂度不可以接受.但我们可以枚举第一行.为什么这么做呢.因为第一行的位置确定后我们以后对每一行处理的时候只要看这一行上面的位置是开着的还是关着的,如果是关的就翻转一次.看最后这样处理完后最后一行是不是全亮着的(因为这么操作已经确保了上面所有行都是开着的).可以的话更新一下答案就好了.
附上代码(非递归形式)
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int a[5][5],tmp[5][5];
int dr[4] = {
0,0,-1,1};
int dc[4] = {
1,-1,0,0};
bool inside(int r,int c){
if(r >=0 && r<5 && c >=0 && c<5) return true;
return false;
}
void flip(int r,int c){
tmp[r][c] = !tmp[r][c];
for(int i=0;i<4;++i) if(inside(r+dr[i],c+dc[i])) tmp[dr[i]+r][c+dc[i]] = !tmp[dr[i]+r][c+dc[i]];
}
int main(){
int t;
cin >> t;
while(t--){
int ans = 1e6;
for(int i=0;i<5;++i){
string s;
cin >> s;
for(int j=0;j<5;++j){
if(s[j] == '0') a[i][j] = 0;
else a[i][j] = 1;
}
}
for(int k=0;k<(1 << 5);++k){
int cnt = 0;
for(int i=0;i<5;++i) for(int j=0;j<5;++j) tmp[i][j] = a[i][j];
for(int i=0;i<5;++i) if((1 << i) & k) flip(0,i),cnt++;
for(int i=1;i<5;++i){
for(int j=0;j<5;++j){
if(!tmp[i-1][j]) flip(i,j),cnt++;
}
}
bool f= 1;
for(int i=0;i<5;++i) if(!tmp[4][i]){
f = 0;break;}
if(f) ans = min(cnt,ans);
}
if(ans <= 6) cout << ans << endl;
else cout << -1 << endl;
}
return 0;
}
9.奇怪的汉诺塔
4塔n盘问题.一开始看到很懵逼.想了几种递推方式都是错的.最后看了题解才明白.题解:先把前i个盘在四塔模式下移动到B上,这样的话剩下n-i个盘就变成了三塔模式,因为这i个盘子是最小的那几个.等于B柱是废的.用不了.所以答案就是min(2×f[i]+d[n-i]).三塔模式就很好求了. d[i] = 2×d[n-1]+1.
代码
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 1e9+7;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
int dp[13],d[13];
int main(){
memset(dp,0x3f,sizeof(dp));
dp[1] = 1;
d[1] = 1;
cout << 1 << endl;
for(int i=2;i<=12;++i){
d[i] = 2*d[i-1]+1;
for(int j=1;j<i;++j){
dp[i] = min(dp[i],dp[j]*2+d[i-j]);
}
cout << dp[i] << endl;
}
return 0;
}
10.约数之和.
一道经典的分治题.具体方法书上写的很清楚了.讨论一下偶数情况怎么算最快.其实知道奇数算法了,偶数可以转换成奇数形式.就是(1+p1…pn-1)+pn就变成奇数+pn的形式了.有个坑点是a有可能是0,需要特判一下.
代码
#define LL long long
#define pb push_back
#define pii pair<int,int>
#define pll pair<long long,long long>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 9901;
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
LL qpow(LL a,LL b){
LL res = 1;
while(b){
if(b&1) res = res*a%mod;
a = a*a%mod;
b >>= 1;
}
return res;
}
LL sum(LL p,LL k){
if(!k) return 1;
if(k&1) return (sum(p,(k-1)/2)*(qpow(p,(k+1)/2)+1)) %mod;
else return (sum(p,k-1) + qpow(p,k))%mod;
}
int main(){
LL a,b;
cin >> a >> b;
LL res = 1;
if(!a) res