程序员进阶之算法练习(六十一)

正文

题目1

题目链接
题目大意:
给出正整数n,求整数1到整数n之中,有多少整数是由单一的字符组成,比如说1 , 77, 777, 44 和 999999 就是符合要求的整数。
整数1到18中,只有 1, 2, 3, 4, 5, 6, 7, 8, 9 和 11符合要求。

输入:
第一行整数𝑡,表示有t个样例 (1≤𝑡≤1e4)
每个样例一行,𝑛 (1≤𝑛≤1e9)

输出:
每个样例一行,输出1到n之间有多少个数字符合要求。

input
6
18
1
9
100500
33
1000000000

output
10
1
9
45
12
81

题目解析:
纯暴力枚举,则是从1、2、3开始算,把每个数字拆解成字符串数组,然后判断数字是否相同的;
但是由于n=1e9,数据量较大,容易在字符串转化的过程中超时。

换一种思路,只枚举符合要求的部分,比如说先考虑1位数、再考虑2位数、然后是3位数,知道数字比n大。

char a[N];

int main(int argc, const char * argv[]) {
    // insert code here...
    int t;
    cin >> t;
    while (t--) {
        cin >> a;
        int n = strlen(a);
        
        int ans = (n - 1) * 9 + (a[0] - '0');
        int ok = 1;
        for (int i = 1; i < n; ++i) {
            if (a[i] < a[0]) {
                ok = 0;
                break;
            }
            else if (a[i] > a[0]) {
                ok = 1;
                break;
            }
        }
        if (!ok) {
            --ans;
        }
        
        cout << ans << endl;
    }
           
    
    return 0;
}

题目2

题目链接
题目大意:
给出n个整数 𝑎1,𝑎2,…,𝑎𝑛;
每次可以选择一个偶数c,将数组中所有等于c的数字除以2;
比如说数组[6,8,12,6,3,12] 选择数字6,将6除以2之后得到[3,8,12,3,3,12]
问现在最少需要操作多少次,才能将所有的数字转化成奇数;

输入:
第一行整数𝑡,表示有t个样例 (1≤𝑡≤1e4)
每个样例有2行:
第一行整数𝑛 (1≤𝑛≤2⋅1e5)
第二行𝑎1,𝑎2,…,𝑎𝑛 (1≤𝑎𝑖≤1e9)

输出:
每个样例一行,输出最小的操作次数。

input
4
6
40 6 40 3 20 1
1
1024
4
2 4 8 16
3
3 1 7
output
4
10
4
0

题目解析:
每次可以选择一个数字x,将数组所有的x=x/2;

通过贪心的思想可以知道,每次会选择最大的数字开始贪心;
并且数字出现个数的多少没有影响,那么可以采用这样的策略:
1、把数组中的数字排序,从大到小开始遍历;
2、每次拿出最大的数字,如果是奇数则直接通过;如果是偶数,则需要++ans,进入第3步;
3、判断原来数组里面是否有这个数字;如果有对应数字,则直接通过;如果没有出现过x/2,则把x/2放回数组;
循环处理,直到结束。

int a[N];
map<int, int> h;
vector<int> s;
priority_queue<int> q;

int main(int argc, const char * argv[]) {
    // insert code here...
    int t;
    cin >> t;
    while (t--) {
        int n;
        cin >> n;
        
        h.clear();
        while (!s.empty()) {
            s.pop_back();
        }
        
        for (int i = 0; i < n; ++i) {
            int k;
            scanf("%d", &k);
            ++h[k];
        }
        
        for (map<int, int>::iterator iter = h.begin(); iter != h.end(); ++iter) {
            q.push(iter->first);
        }
        
        int ans = 0;
        while (!q.empty()) {
            int top = q.top();
            q.pop();
            if (top % 2 == 0) {
                if (!h[top/2]) {
                    q.push(top / 2);
                }
                ++ans;
            }
        }
        
        cout << ans << endl;
        
    }
           
    
    return 0;
}

题目3

题目链接
题目大意:
给出一个字符串s,现在不希望字符串里面出现one和two这两个单词,比如说"oneee", "ontwow", "twone" and "oneonetwo"都是不好的字符串。
现在可以从字符串中选择若干个位置,去掉这些位置上的字符。
问最少去掉多少个字符。

输入:
第一行𝑡,表示t个样例 (1≤𝑡≤1e4)
每个样例一行,字符串s,长度不超过 1.5⋅1e5

输出:
每个样例2行
第一行,整数k表示需要去掉k个位置
第二行,k个整数,表示k个需要去掉字符的位置。

input
4
onetwone
testme
oneoneone
twotwo

output
2
6 3
0

3
4 1 7
2
1 4

题目解析:
对于某个字符结尾,可能有normal、o、on、t、tw这几种情况,分别表示为状态0、1、2、3、4;
那么dp[i][j]表示,第i个字符,以状态j结尾时,需要去掉的最少字符数量;

对于第i个字符,都可以有dp[i][j] = dp[i-1][j] + 1;
同时,根据字符内容,有特定的转移方案,这部分转移方程较多,可以直接思考代码。

思考🤔:
这个题目也可以使用贪心的做法,假如只有one这个单词,那么直接去掉n是最优解:因为去掉头尾会有oone和onee的bad case存在,去掉n会直接破坏one这个单词;
但是由于有两个字符存在,会导致出现twone的情况导致去掉n仍然存在two的情况;针对这种特殊情况,是需要去掉中间的o;
这种贪心的做法相对动态规划比较容易实现。

string a;
int dp[N][5];
pair<int, int> last[N][5];

void compare(int i, int j, int x, int y) {
    if (dp[i][j] > dp[x][y]) {
        dp[i][j] = dp[x][y];
        last[i][j] = make_pair(x, y);
    }
}

int main(int argc, const char * argv[]) {
    // insert code here...
    int t;
    cin >> t;
    while (t--) {
        cin >> a;
        int n = (int)a.length();
        
        for (int j = 0; j < 5; ++j) {
            dp[0][j] = n;
            last[0][j] = make_pair(0, 0);
        }
        if (a[0] == 'o') {
            dp[0][1] = 0;
        }
        else if (a[0] == 't') {
            dp[0][3] = 0;
        }
        else {
            dp[0][0] = 0;
        }
        
        for (int i = 1; i < n; ++i) {
            // 不取a[i]
           for (int j = 0; j < 5; ++j) {
               dp[i][j] = dp[i - 1][j] + 1;
               last[i][j] = make_pair(i - 1, j);
            }
            
            // 取a[i], normal、o、on、t、tw这几种情况,分别表示为状态0、1、2、3、4;
            if (a[i] == 'o') {
                compare(i, 1, i-1, 0);
                compare(i, 1, i-1, 1);
                compare(i, 1, i-1, 2);
                compare(i, 1, i-1, 3);
            }
            else if (a[i] == 'n') {
                compare(i, 2, i - 1, 1);
                
                compare(i, 0, i-1, 0);
                compare(i, 0, i-1, 2);
                compare(i, 0, i-1, 3);
                compare(i, 0, i-1, 4);
            }
            else if (a[i] == 't') {
                compare(i, 3, i-1, 0);
                compare(i, 3, i-1, 1);
                compare(i, 3, i-1, 2);
                compare(i, 3, i-1, 3);
                compare(i, 3, i-1, 4);
            }
            else if (a[i] == 'w') {
                compare(i, 4, i-1, 3);
                
                compare(i, 0, i-1, 0);
                compare(i, 0, i-1, 1);
                compare(i, 0, i-1, 2);
                compare(i, 0, i-1, 4);
            }
            else if (a[i] == 'e') {
                compare(i, 0, i-1, 0);
                compare(i, 0, i-1, 1);
                compare(i, 0, i-1, 3);
                compare(i, 0, i-1, 4);
            }
            else {
                compare(i, 0, i-1, 0);
                compare(i, 0, i-1, 1);
                compare(i, 0, i-1, 2);
                compare(i, 0, i-1, 3);
                compare(i, 0, i-1, 4);
            }
        }
        long index = min_element(dp[n - 1], dp[n - 1] + 5) - dp[n - 1];
        
        cout << dp[n-1][index] << endl;
        
        long x = n - 1, y = index;
        while (x != 0 || y != 0) {
            pair<int, int> fp = last[x][y];
            if (dp[fp.first][fp.second] < dp[x][y]) {
                printf("%d ", x + 1);
            }
            x = fp.first, y = fp.second;
        }
        puts("");
    }
           
    
    return 0;
}

题目4

题目链接
题目大意:
一个整数是2的x次幂,或者是x的阶乘,那么这个数字是powerful的;
现在给出一个整数n,希望由若干个powerful的整数相加来得到n,要求:
1、没有相同的powerful整数;
2、powerful整数尽可能少;

输入:
第一行样例数𝑡 (1≤𝑡≤100).
接下来每个样例一行𝑛 (1≤𝑛≤1e12).

输出:
能相加得到n的最少powerful整数的数量,如果没有答案则输出-1;

Examples
input
4
7
11
240
17179869184
output
2
3
4
1

题目解析:
首先把powerful的整数枚举取出来:
2的x次幂:1、2、4、8、16、32、64、128等;
x的阶乘:1、2、6、24、120等;
这两部分数字可以通过hash去重,得到1、2、4、8、16、24、32、64、120、128等;
题目的要求就是从这些数字中选出最少的数字,来组成整数n;
n个物品的选择来形成特定大小,这个是典型的动态规划问题,但是如果用背包的解法,会由于状态数过多而超时;

仔细分析题目,2的x次幂这部分数字,其实已经可以足够组成任意数字;
那么题目就变成了,要在阶乘中选择多少个数字来组成n,剩下的部分可以有2的x次幂来组成(并且这部分的最优解就是数字的二进制位数);
阶乘递增比较快,在15!的时候就超过了题目要求,再减去最开始的阶乘1和2,最终可以不超过2^13=8000左右种结果;
枚举这些结果,结合剩下数字的二进制长度,来得到最优解;

class Solution {
   static const int N = 10010;
   string str;
   vector<lld> num;
   
   lld count(lld n) {
       lld ret = 0;
       while (n) {
           ret += n % 2;
           n /= 2;
       }
       return ret;
   }

public:
   void solve() {
       int t;
       cin >> t;
       
       lld tmp = 2;
       for (int i = 3; i < 15; ++i) {
           tmp *= i;
           num.push_back(tmp);
       }
       
       while (t--) {
           lld n;
           cin >> n;
           lld ans = n;
           for (int i = 0; i < (1 << num.size()); ++i) {
               lld sum = 0;
               lld index = 0;
               int tmp = i;
               while (tmp) {
                   if (tmp % 2) {
                       sum += num[index];
                   }
                   tmp /= 2;
                   ++index;
               }
               if (sum <= n) {
                   ans = min(ans, count(i) + count(n - sum));
               }
           }
           cout << ans << endl;
       }
   }
}
ac;

题目5

题目链接
题目大意:
给出一个有n个节点的树,现在可以把1、2、3·····n的整数赋值到每一个节点上;
一个节点可以被称为good节点,如果它满足:相邻点的数字之和等于自己节点的数字;
现在需要给每个节点赋值,要求整棵树有尽可能多的good节点,如果有多种方案则输出数字和最小的方案;

输入:
第一行𝑛 (2≤𝑛≤2e5)
接下来有n-1行,每行两个整数 𝑢 and 𝑣 表示相邻的节点(1≤𝑢,𝑣≤𝑛)

输出:
先输出最多的good节点数和总的数字和;
接下来n个数字, 𝑤1,𝑤2,…,𝑤𝑛分别表示n个节点的数字 (1≤𝑤𝑖≤1e9)

Examples
input
2
1 2
output
2 2
1 1
input
9
3 4
7 6
2 1
8 3
5 6
1 8
8 6
9 6
output
6 11
1 1 1 1 1 1 1 3 1

题目解析:
由题目的要求,首先要求是尽可能多的good节点,那么叶子节点肯定都会标位1;
其次叶子的相邻节点也一定是1(为了满足good节点的要求),如果有节点和两个叶子相邻节点连接,那么这个节点也有机会成为good节点,如下:
五个连乘一条直线节点:1-1-2-1-1

由此我们知道,每个节点有两种状态isGood=1和0,信息有maxNode子树最大节点数,minWeight子树最小数字和;
而且我们按照题目可以得到两个状态转移的条件,对于节点u:
1、假如u是good节点,那么和u相邻的节点一定不是good节点;
2、假如u不是good节点,那么和u相邻的节点可以是good节点,也可以不是good节点;

由于上面的条件2,我们知道这个题目无法使用贪心的思想,因为贪心需要决策路径是唯一的,但是由于非good节点节点是可以相邻的,导致出现了分支;
针对这个情况,我们可以用动态规划来解决。
首先设定好状态,pair<int, int> dp[n][2]:n个节点,每个节点有2个状态,每个节点需要关注两个信息;
dp[i][0]表示第i个节点不为good的子树最优解,dp[i][1]为第i个节点为good的子树最优解,first是good数,second是子树和;
再由上面的状态转移条件,可以得到转移方程。

接着从节点1开始dfs,对于每一个节点u和字节点v,计算得到dp[u][0]和dp[u][1];
同时需要记录动态规划的决策过程,因为最终答案还要回溯决策,从而得到结果。

思考🤔:
这个题目的输出略微麻烦,但是记录清楚决策分支,再用dfs回溯还是可以解决的。

class Solution {
    static const int N = 200010;
    vector<int> g[N];
    int vis[N];
    vector<int> last[N];
    int ans[N];
    pair<int, int> dp[N][2]; // dp[i][0]表示第i个节点不为good的子树最优解,dp[i][1]为第i个节点为good的子树最优解,first是good数,second是子树和
    void add_pair(pair<int, int> &x, pair<int, int> &y) {
        x.first += y.first;
        x.second += y.second;
    }
    
    bool cmp_pair(pair<int, int> &x, pair<int, int> &y) {
        return x.first > y.first || (x.first == y.first && x.second < y.second);
    }
    
    void dfs(int u) {
        vis[u] = 1;
        for (int i = 0; i < g[u].size(); ++i) {
            int v = g[u][i];
            if (!vis[v]) {
                dfs(v);
            }
        }
        
        for (int i = 0; i < g[u].size(); ++i) {
            int v = g[u][i];
            if (cmp_pair(dp[v][0], dp[v][1])) {
                add_pair(dp[u][0], dp[v][0]);
                last[u].push_back(0);
            }
            else {
                add_pair(dp[u][0], dp[v][1]);
                last[u].push_back(1);
            }
            add_pair(dp[u][1], dp[v][0]);
        }
        dp[u][1].first += 1; // 自己作为good节点
        dp[u][1].second += g[u].size(); // good节点代价,自重
        
        dp[u][0].second += 1; // 自重
//        cout << u << " " << dp[u][0].first << " " << dp[u][0].second  << " " << dp[u][1].first << " " << dp[u][1].second << endl;
    }
    
    
    void dfs_ans(int u, bool isGood) {
        vis[u] = 1;
        for (int i = 0; i < g[u].size(); ++i) {
            int v = g[u][i];
            if (!vis[v]) {
                dfs_ans(v, isGood ? 0 : last[u][i]);
            }
        }
        
        if (isGood) {
            ans[u] = g[u].size();
        }
        else {
            ans[u] = 1;
        }
    }
    
    


public:
    void solve() {
        int n;
        cin >> n;
        for (int i = 1; i < n; ++i) {
            int x, y;
            cin >> x >> y;
            g[x].push_back(y);
            g[y].push_back(x);
        }
        if (n == 2) {
            cout << "2 2\n1 1" << endl;
        }
        else {
            dfs(1);
            memset(vis, 0, sizeof(vis));
            if (cmp_pair(dp[1][0], dp[1][1])) {
                cout << dp[1][0].first << " " << dp[1][0].second << endl;
                dfs_ans(1, 0);
            }
            else {
                cout << dp[1][1].first << " " << dp[1][1].second << endl;
                dfs_ans(1, 1);
            }
            for (int i = 1; i <= n; ++i) {
                cout << ans[i] << " ";
            }
            cout << endl;
        }
    }
}
ac;

总结

这次题目都比较有意思,题目4和题目5有点难度,尤其是题目5的输出。题目5的结果输出不难想出办法,就是挺麻烦,不够优雅。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容