笔试编程算法题笔记(三 C++代码)

1.kotori和n皇后

 题意简单来说就是,在一个无穷大的棋盘上,不断插入k个皇后,皇后们如果在 同一行,同一列,在一个45°主对角线 在一个135°副对角线上,就可以互相攻击。

我们需要判断在第i个皇后插入后,是否存在互相攻击的情况。

解法:哈希表。

我们只需要将皇后的攻击范围存入哈希表(unordered_set)中,每次插入时判断这个皇后是否在这个攻击范围内即可。

因此我们需要创建四个哈希表。对应的行 列,两个对角线。

行和列很好理解,关于对角线

比如主对角线,如果y - x相等,说明在同一个主对角线上。同理,如果y + x相等,说明在同一个副对角线上。

最后,我们只需要记录下第一次出现皇后相互攻击的时机即可。

代码:

#include <iostream>
#include <unordered_set>

using namespace std;

int main()
{
    int k,t;
    cin >> k;
    int ret = 0x3f3f3f3f;
    unordered_set <int> row;
    unordered_set <int> low;
    unordered_set <int> dig1;// 主对角线
    unordered_set <int> dig2;// 副对角线
    
    int a,b;
    for(int i = 0; i < k; ++i)
    {
        cin >> a >> b;
        if(row.count(a) || low.count(b) || dig1.count(b - a) || dig2.count(b + a))
        {
            if(ret == 0x3f3f3f3f)
            {
                ret = i + 1;
            }
        }
        else 
        {
            row.insert(a);
            low.insert(b);
            dig1.insert(b - a);
            dig2.insert(b + a);
        }
    }
    
    cin >> t;
    int tmp;
    for(int i = 0; i < t; ++i)
    {
        cin >> tmp;
        if(tmp >= ret) cout << "Yes" << endl;
        else cout << "No" << endl;
    }
    return 0;
}

 2.取金币

 这是一道比较难的区间dp问题。

题意简单,图中很容易理解。

状态表示:

dp[i][j]表示区间[i,j]的金币全部拿走,能获得的最大积分是多少。

状态转移方程:

假设我们取了[i,j]区间的第k堆金币,那么此时区间[i,j]能获得的最大积分也就是dp[i][j] = 

dp[i][k - 1] + dp[k + 1][j] + arr[k] * arr[i - 1] * arr[j + 1]。( i <= k <= j )

记得每次要取最大值。

然后我们再来考虑一下边界情况

首先是对于源数组的,我们可以给源数组的左右两边都新增一个元素1,这样可以在不影响计算的情况下防止数组越界。

然后对于dp表,我们可以多开两行和多开两列, 因为源数组我们新增了两个元素1,所以多开两行和两列可以避免填表时数组越界,dp表都初始化为0不影响计算结果。

并且我们的填表总是在对角线及以上填表的。因为 i >= j。

再来看填表顺序,跟以往不同,我们要对照着状态转移方程来看

dp[i][k - 1] + dp[k + 1][j] + arr[k] * arr[i - 1] * arr[j + 1]。( i <= k <= j )

我们发现填表需要用到j + 1,要用到i - 1,所以填表顺序为从下往上,从左往右填。 

最后返回dp[1,n]即可。

代码:

class Solution {
public:
    int arr[110];
    int dp[110][110];
    int getCoins(vector<int>& coins) {
        int n = coins.size();
        arr[0] = arr[n + 1] = 1;
        for(int i = 1; i <= n; ++i) arr[i] = coins[i - 1]; // 新增两个元素

        for(int i = n; i >= 1; --i)
        {
            for(int j = i; j <= n; j++)
            {
                for(int k = i; k <= j; ++k)
                    dp[i][j] = max(dp[i][j],dp[i][k - 1] + dp[k + 1][j] + arr[k] * arr[i - 1] * arr[j + 1]);
            }
        }

        return dp[1][n];
    }
};

3.四个选项

 题意简单来说就是有四个选项,给出四个选项的数量(保证四个选项加起来是12个),要将这些选项填到12个空里面,同时存在m个额外条件,每个条件使得第x个选项要等于第y个选项。

解法:递归 dfs

剪枝有两点:

1.如果当前该选项已经没有数量了,应该剪枝。

2.如果填入该选项发现不满足额外条件,应该剪枝。

细节:

1.可以用cnt[5]数组来存选项的数量,并用下标1 2 3 4 来映射选项的A B C D。

2.用哈希的思想来存额外条件,可以用一个二维的bool类型矩阵来存。

3.可以用一个变长数组来存选项填入的顺序,并事先加入一个占位符,方便对应下标映射关系。

代码:

#include <iostream>
#include <vector>

using namespace std;

int cnt[5];
int m,x,y;
bool same[13][13]; // 记录x,y相等的情况
int ret;
vector<int>path; // 存放路径

bool isSame(int pos,int cur)
{
    for(int i = 1; i < pos; ++i)
    {
        if(same[i][pos] && path[i] != cur) return false;
    }
    return true;
}

void dfs(int pos)
{
    if(pos > 12)
    {
        ret++;
        return;
    }
    
    for(int i = 1; i <= 4; ++i)
    {
        if(cnt[i] == 0) continue; // 剪枝1 该选项数量已为0
        if(!isSame(pos,i)) continue; // 剪枝2 选项不相同
        cnt[i]--;
        path.push_back(i);
        dfs(pos + 1);
        cnt[i]++;
        path.pop_back();
    }
}

int main()
{
    for(int i = 1; i <= 4; ++i) cin >> cnt[i];
    cin >> m;
    while(m--)
    {
        cin >> x >> y;
        same[x][y] = same[y][x] = true;
    }
    
    path.push_back(0); // 先增一个占位符
    dfs(1);
    
    cout << ret << endl;
    return 0;
}

4.接雨水

 本题解法很多,这里采用动态规划(预处理)的解法。

思想:先求出每一根柱子上能接多少雨水,然后把所有柱子能接的雨水累加即可。

那么怎么计算第i根柱子能接多少雨水呢?我们发现,第i根柱子能接的雨水根取决与该柱子左边柱子的最大值和右边柱子的最大值,取二者的较小值就是该柱子能接的雨水量。

所有我们可以用两个数组,left[i],right[i]来记录第i根柱子左边柱子的最大值,和右边柱子的最大值。

也就是前缀最大值。

另外有一个细节:

比如left[i],这个数组表示的是区间[0,i]的最大值,要把i也带进去,因为我们计算每一个柱子的公式是 ret += min(left[i],right[i]) - height[i];  如果没有把i算进去的话,恰好i柱子是最高的,那么结果就会产生负值,是不合理的,最少的雨水量是0。所以我们把i也算进去,才是合理的。

代码:

class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        vector<int> left(n);
        vector<int> right(n);
        left[0] = height[0];
        right[n - 1] = height[n - 1];
        for(int i = 1; i < n; ++i)
            left[i] = max(left[i - 1],height[i]);
        for(int i = n - 2; i >= 0; --i)
            right[i] = max(right[i + 1],height[i]);
        
        int ret = 0;
        for(int i = 0; i < n; ++i)
        {
            ret += min(left[i],right[i]) - height[i];
        }
        
        return ret;
    }
};

5.栈和排序

 

解法:栈+贪心

 贪心思想:优先让当前最大的值出栈。

先创建一个栈

1.依次进栈。

2. 先更新目标值

3.如果当前的栈顶元素大于目标值,说明它是当前能输出的最大值。

另外可以定义一个哈希表,来存元素是否入栈。

class Solution {
public:
    vector<int> solve(vector<int>& a) {
        int n = a.size();
        stack<int> s;
        int aim = n; // 目标值
        vector<int> ret;
        vector<bool> hash(n + 1); 
        
        for(auto x : a)
        {
            s.push(x);
            hash[x] = true;

            while(hash[aim]) // 先更新目标值
            {
                aim--;
            }

            while(s.size() && s.top() >= aim)
            {
                ret.push_back(s.top());
                s.pop();
            }
        }

        return ret;
    }
};

6.加减

 

综合性很强的一道题。

题意简单来说就是给了一个数组,还有最多k次操作机会,每次操作可以使数组的某个元素+1或者-1, 在经过最多k次操作后,使得数组中出现最多相同的数的次数,求出这个元素出现的次数。

思路: 

1.贪心思想:我们要尽可能的选择挨得近的数,这样把它们变成相同的数所花费的次数就会少。

那么我们可以先将数组进行排序,这样就可以让数组的元素尽可能挨得近。

2.之后,我们就可以枚举数组中所有的区间,找出使区间内所有数变成相同的数所花费的代价cost <= k,在这些区间中的最大区间。

如果直接暴力枚举,时间复杂度O(N^2),超时。

在枚举过程中,我们可以发现其left和right指针移动的单调性,所以可以用滑动窗口来优化,那么时间复杂度降为O(N)。

3.关于怎样计算区间的最小花费

这里引入一个数学问题:在一个数轴上有一些点,如何选取一个位置,使得所有的点到这个位置的距离之和最小?

答案是:选取中间的点,如果点的个数是偶数,那么中间任意两个点都可以。

所以将这个结论应用到这题

a1就是left,a5就是right所指向的位置,那么该区间的最少花费也就是所有点到a3的距离。

如果我们每次都要对每个点都计算的话,那么这里计算的时间复杂度为O(N),因此我们可以使用前缀和,把时间复杂度降为O(1)。

用sum数组代表前缀和数组。

原本求cost的公式化简后,下划线表示的是这个地方可以用前缀和来代替,并且发现了一个规律,其余的部分就是 (a3前有多少个点) * a3 - (a3后有多少个点) * a3 - (前缀和1) + (前缀和2) 

那么此时最小cost的计算公式为:

 这个公式对应了原本求cost化简后的例子。

并且发现其中还有可以化简的地方,我们在代码中体现了。 

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

long long arr[N];
long long sum[N]; // 前缀和数组
long long n,k;

long long cal(int l,int r) // 计算区间的花费
{
    int mid = l + (r - l) / 2;
    return (2 * mid - l - r) * arr[mid] - (sum[mid - 1] - sum[l - 1]) + (sum[r] - sum[mid]);
}

int main()
{
    cin >> n >> k;
    for(int i = 1; i <= n; ++i) // 使用了前缀和,那么下标从1开始比较方便
        cin >> arr[i];
    
    sort(arr + 1,arr + n + 1); // 使得元素之间挨得近
    
    for(int i = 1; i <= n; ++i) // 下标从1开始,可以防止越界
        sum[i] = sum[i - 1] + arr[i]; // 前缀和
    
    int left = 1,right = 1;
    int ret = 1;
    long long cost; // 区间的所需要花费的次数
    while(right <= n)
    {
        // 进窗口
        cost = cal(left,right);
        
        while(cost > k) // 判断
        {
            left++; // 出窗口
            cost = cal(left,right);
        }
        //更新结果
        ret = max(ret,right - left + 1);
        right++;
    }
    cout << ret << endl;
    
    return 0;
}

 所以本题的时间复杂度为 O(N logN)主要是排序所消耗的时间。

运用了枚举 + 前缀和 + 滑动窗口 + 贪心。并且还有数学问题的推导,公式的化简。

7.mari和shiny 

 

解法:动态规划

是多状态的线性dp。

其实只要把状态表示想明白就是一道比较简单的dp。

如果我们要找到能组成 ‘shy’的子序列,那么首先得知道有多少个 ‘sh’ 子序列,如果想知道有多少个 ‘sh’子序列,就需要知道有 多少个 's'。

那么就有三个状态,那么就存在三个dp表。

s[i] 表示在[0,i]区间有多少个 's'。

h[i] 表示在[0,i]区间有多少个 'sh'。

y[i]表示在[0,i]区间有多少个'shy'。

因为它的状态转移方程很简单,先填好s表,然后再填h表,最后再填y表,然后返回y表的结果。

所以就不多说了。三次for循环搞定。
另外有一个细节就是初始化,我们可以多开一个格子,方便填表。

还有就是本题数据量比较大,记得用 long long 类型来存。

代码:

#include <iostream>

using namespace std;

const int N = 3e5 + 10;
long long s[N],h[N],y[N];

int main()
{
    int n;
    string tmp;
    cin >> n >> tmp;

    for(int i = 1; i <= n; ++i)
    {
        s[i] = s[i - 1];
        if(tmp[i - 1] == 's')
            s[i] += 1;
    }
    for(int i = 1; i <= n; ++i)
    {
        h[i] = h[i - 1];
        if(tmp[i - 1] == 'h')
            h[i] += s[i];
    }
    for(int i = 1; i <= n; ++i)
    {
        y[i] = y[i - 1];
        if(tmp[i - 1] == 'y') 
            y[i] += + h[i];
    }
    cout << y[n] << endl;
    
    return 0;
}

  • 21
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值